From 9ad839c7766bd5d6121a10b2c306d6fef9666c52 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 12 May 2020 22:10:35 -0400 Subject: Initial migration code --- .../Data/SqliteDisplayPreferencesRepository.cs | 225 --------------------- 1 file changed, 225 deletions(-) delete mode 100644 Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs (limited to 'Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs') diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs deleted file mode 100644 index d474f1c6b..000000000 --- a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs +++ /dev/null @@ -1,225 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Text.Json; -using System.Threading; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Json; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; - -namespace Emby.Server.Implementations.Data -{ - /// - /// Class SQLiteDisplayPreferencesRepository. - /// - public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository - { - private readonly IFileSystem _fileSystem; - - private readonly JsonSerializerOptions _jsonOptions; - - public SqliteDisplayPreferencesRepository(ILogger logger, IApplicationPaths appPaths, IFileSystem fileSystem) - : base(logger) - { - _fileSystem = fileSystem; - - _jsonOptions = JsonDefaults.GetOptions(); - - DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db"); - } - - /// - /// Gets the name of the repository. - /// - /// The name. - public string Name => "SQLite"; - - public void Initialize() - { - try - { - InitializeInternal(); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error loading database file. Will reset and retry."); - - _fileSystem.DeleteFile(DbFilePath); - - InitializeInternal(); - } - } - - /// - /// Opens the connection to the database - /// - /// Task. - private void InitializeInternal() - { - string[] queries = - { - "create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)", - "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)" - }; - - using (var connection = GetConnection()) - { - connection.RunQueries(queries); - } - } - - /// - /// Save the display preferences associated with an item in the repo - /// - /// The display preferences. - /// The user id. - /// The client. - /// The cancellation token. - /// item - public void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken) - { - if (displayPreferences == null) - { - throw new ArgumentNullException(nameof(displayPreferences)); - } - - if (string.IsNullOrEmpty(displayPreferences.Id)) - { - throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences)); - } - - cancellationToken.ThrowIfCancellationRequested(); - - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => SaveDisplayPreferences(displayPreferences, userId, client, db), - TransactionMode); - } - } - - private void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, IDatabaseConnection connection) - { - var serialized = JsonSerializer.SerializeToUtf8Bytes(displayPreferences, _jsonOptions); - - using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)")) - { - statement.TryBind("@id", new Guid(displayPreferences.Id).ToByteArray()); - statement.TryBind("@userId", userId.ToByteArray()); - statement.TryBind("@client", client); - statement.TryBind("@data", serialized); - - statement.MoveNext(); - } - } - - /// - /// Save all display preferences associated with a user in the repo - /// - /// The display preferences. - /// The user id. - /// The cancellation token. - /// item - public void SaveAllDisplayPreferences(IEnumerable displayPreferences, Guid userId, CancellationToken cancellationToken) - { - if (displayPreferences == null) - { - throw new ArgumentNullException(nameof(displayPreferences)); - } - - cancellationToken.ThrowIfCancellationRequested(); - - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => - { - foreach (var displayPreference in displayPreferences) - { - SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db); - } - }, - TransactionMode); - } - } - - /// - /// Gets the display preferences. - /// - /// The display preferences id. - /// The user id. - /// The client. - /// Task{DisplayPreferences}. - /// item - public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, Guid userId, string client) - { - if (string.IsNullOrEmpty(displayPreferencesId)) - { - throw new ArgumentNullException(nameof(displayPreferencesId)); - } - - var guidId = displayPreferencesId.GetMD5(); - - using (var connection = GetConnection(true)) - { - using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client")) - { - statement.TryBind("@id", guidId.ToByteArray()); - statement.TryBind("@userId", userId.ToByteArray()); - statement.TryBind("@client", client); - - foreach (var row in statement.ExecuteQuery()) - { - return Get(row); - } - } - } - - return new DisplayPreferences - { - Id = guidId.ToString("N", CultureInfo.InvariantCulture) - }; - } - - /// - /// Gets all display preferences for the given user. - /// - /// The user id. - /// Task{DisplayPreferences}. - /// item - public IEnumerable GetAllDisplayPreferences(Guid userId) - { - var list = new List(); - - using (var connection = GetConnection(true)) - using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId")) - { - statement.TryBind("@userId", userId.ToByteArray()); - - foreach (var row in statement.ExecuteQuery()) - { - list.Add(Get(row)); - } - } - - return list; - } - - private DisplayPreferences Get(IReadOnlyList row) - => JsonSerializer.Deserialize(row[0].ToBlob(), _jsonOptions); - - public void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken) - => SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken); - - public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client) - => GetDisplayPreferences(displayPreferencesId, new Guid(userId), client); - } -} -- cgit v1.2.3 From 3eeb6576d8425c8d2917f861b466dfa36e3994df Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Fri, 15 May 2020 17:24:01 -0400 Subject: Migrate User DB to EF Core --- Emby.Server.Implementations/ApplicationHost.cs | 1 - .../Data/SqliteDisplayPreferencesRepository.cs | 225 ++++++ .../Session/SessionManager.cs | 35 +- Jellyfin.Data/Entities/AccessSchedule.cs | 18 +- Jellyfin.Data/Entities/Group.cs | 41 +- Jellyfin.Data/Entities/Permission.cs | 53 +- Jellyfin.Data/Entities/Preference.cs | 81 +-- Jellyfin.Data/Entities/User.cs | 141 ++-- Jellyfin.Data/IHasPermissions.cs | 10 + Jellyfin.Server.Implementations/JellyfinDb.cs | 10 +- .../JellyfinDbProvider.cs | 2 +- .../20200504195702_UserSchema.Designer.cs | 324 --------- .../Migrations/20200504195702_UserSchema.cs | 219 ------ .../Migrations/20200517002411_AddUsers.Designer.cs | 404 +++++++++++ .../Migrations/20200517002411_AddUsers.cs | 297 ++++++++ .../Migrations/JellyfinDbModelSnapshot.cs | 186 +++-- .../User/DefaultAuthenticationProvider.cs | 185 ----- .../User/DefaultPasswordResetProvider.cs | 126 ---- .../User/DeviceAccessEntryPoint.cs | 65 -- .../User/InvalidAuthProvider.cs | 47 -- .../User/UserManager.cs | 771 --------------------- .../Users/DefaultAuthenticationProvider.cs | 185 +++++ .../Users/DefaultPasswordResetProvider.cs | 127 ++++ .../Users/DeviceAccessEntryPoint.cs | 66 ++ .../Users/InvalidAuthProvider.cs | 47 ++ .../Users/UserManager.cs | 763 ++++++++++++++++++++ Jellyfin.Server/CoreAppHost.cs | 6 + Jellyfin.Server/Jellyfin.Server.csproj | 1 - .../Migrations/Routines/MigrateUserDb.cs | 100 ++- MediaBrowser.Model/Users/UserPolicy.cs | 8 +- 30 files changed, 2526 insertions(+), 2018 deletions(-) create mode 100644 Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs create mode 100644 Jellyfin.Data/IHasPermissions.cs delete mode 100644 Jellyfin.Server.Implementations/Migrations/20200504195702_UserSchema.Designer.cs delete mode 100644 Jellyfin.Server.Implementations/Migrations/20200504195702_UserSchema.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20200517002411_AddUsers.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20200517002411_AddUsers.cs delete mode 100644 Jellyfin.Server.Implementations/User/DefaultAuthenticationProvider.cs delete mode 100644 Jellyfin.Server.Implementations/User/DefaultPasswordResetProvider.cs delete mode 100644 Jellyfin.Server.Implementations/User/DeviceAccessEntryPoint.cs delete mode 100644 Jellyfin.Server.Implementations/User/InvalidAuthProvider.cs delete mode 100644 Jellyfin.Server.Implementations/User/UserManager.cs create mode 100644 Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs create mode 100644 Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs create mode 100644 Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs create mode 100644 Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs create mode 100644 Jellyfin.Server.Implementations/Users/UserManager.cs (limited to 'Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index b2fe6a3ab..56b103763 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -562,7 +562,6 @@ namespace Emby.Server.Implementations // TODO: Refactor to eliminate the circular dependency here so that Lazy isn't required serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); - serviceCollection.AddSingleton(); // TODO: Refactor to eliminate the circular dependency here so that Lazy isn't required // TODO: Add StartupOptions.FFmpegPath to IConfiguration and remove this custom activation diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs new file mode 100644 index 000000000..63d0321b7 --- /dev/null +++ b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs @@ -0,0 +1,225 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text.Json; +using System.Threading; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Json; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; +using SQLitePCL.pretty; + +namespace Emby.Server.Implementations.Data +{ + /// + /// Class SQLiteDisplayPreferencesRepository. + /// + public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository + { + private readonly IFileSystem _fileSystem; + + private readonly JsonSerializerOptions _jsonOptions; + + public SqliteDisplayPreferencesRepository(ILogger logger, IApplicationPaths appPaths, IFileSystem fileSystem) + : base(logger) + { + _fileSystem = fileSystem; + + _jsonOptions = JsonDefaults.GetOptions(); + + DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db"); + } + + /// + /// Gets the name of the repository. + /// + /// The name. + public string Name => "SQLite"; + + public void Initialize() + { + try + { + InitializeInternal(); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error loading database file. Will reset and retry."); + + _fileSystem.DeleteFile(DbFilePath); + + InitializeInternal(); + } + } + + /// + /// Opens the connection to the database + /// + /// Task. + private void InitializeInternal() + { + string[] queries = + { + "create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)", + "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)" + }; + + using (var connection = GetConnection()) + { + connection.RunQueries(queries); + } + } + + /// + /// Save the display preferences associated with an item in the repo + /// + /// The display preferences. + /// The user id. + /// The client. + /// The cancellation token. + /// item + public void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken) + { + if (displayPreferences == null) + { + throw new ArgumentNullException(nameof(displayPreferences)); + } + + if (string.IsNullOrEmpty(displayPreferences.Id)) + { + throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences)); + } + + cancellationToken.ThrowIfCancellationRequested(); + + using (var connection = GetConnection()) + { + connection.RunInTransaction( + db => SaveDisplayPreferences(displayPreferences, userId, client, db), + TransactionMode); + } + } + + private void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, IDatabaseConnection connection) + { + var serialized = JsonSerializer.SerializeToUtf8Bytes(displayPreferences, _jsonOptions); + + using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)")) + { + statement.TryBind("@id", new Guid(displayPreferences.Id).ToByteArray()); + statement.TryBind("@userId", userId.ToByteArray()); + statement.TryBind("@client", client); + statement.TryBind("@data", serialized); + + statement.MoveNext(); + } + } + + /// + /// Save all display preferences associated with a user in the repo + /// + /// The display preferences. + /// The user id. + /// The cancellation token. + /// item + public void SaveAllDisplayPreferences(IEnumerable displayPreferences, Guid userId, CancellationToken cancellationToken) + { + if (displayPreferences == null) + { + throw new ArgumentNullException(nameof(displayPreferences)); + } + + cancellationToken.ThrowIfCancellationRequested(); + + using (var connection = GetConnection()) + { + connection.RunInTransaction( + db => + { + foreach (var displayPreference in displayPreferences) + { + SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db); + } + }, + TransactionMode); + } + } + + /// + /// Gets the display preferences. + /// + /// The display preferences id. + /// The user id. + /// The client. + /// Task{DisplayPreferences}. + /// item + public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, Guid userId, string client) + { + if (string.IsNullOrEmpty(displayPreferencesId)) + { + throw new ArgumentNullException(nameof(displayPreferencesId)); + } + + var guidId = displayPreferencesId.GetMD5(); + + using (var connection = GetConnection(true)) + { + using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client")) + { + statement.TryBind("@id", guidId.ToByteArray()); + statement.TryBind("@userId", userId.ToByteArray()); + statement.TryBind("@client", client); + + foreach (var row in statement.ExecuteQuery()) + { + return Get(row); + } + } + } + + return new DisplayPreferences + { + Id = guidId.ToString("N", CultureInfo.InvariantCulture) + }; + } + + /// + /// Gets all display preferences for the given user. + /// + /// The user id. + /// Task{DisplayPreferences}. + /// item + public IEnumerable GetAllDisplayPreferences(Guid userId) + { + var list = new List(); + + using (var connection = GetConnection(true)) + using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId")) + { + statement.TryBind("@userId", userId.ToByteArray()); + + foreach (var row in statement.ExecuteQuery()) + { + list.Add(Get(row)); + } + } + + return list; + } + + private DisplayPreferences Get(IReadOnlyList row) + => JsonSerializer.Deserialize(row[0].ToBlob(), _jsonOptions); + + public void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken) + => SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken); + + public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client) + => GetDisplayPreferences(displayPreferencesId, new Guid(userId), client); + } +} diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 84b16f1c7..54c26a8a5 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; @@ -14,7 +15,6 @@ using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; @@ -27,6 +27,7 @@ using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; +using Episode = MediaBrowser.Controller.Entities.TV.Episode; namespace Emby.Server.Implementations.Session { @@ -254,7 +255,7 @@ namespace Emby.Server.Implementations.Session string deviceId, string deviceName, string remoteEndPoint, - Jellyfin.Data.Entities.User user) + User user) { CheckDisposed(); @@ -438,7 +439,7 @@ namespace Emby.Server.Implementations.Session string deviceId, string deviceName, string remoteEndPoint, - Jellyfin.Data.Entities.User user) + User user) { CheckDisposed(); @@ -457,7 +458,7 @@ namespace Emby.Server.Implementations.Session sessionInfo.UserId = user?.Id ?? Guid.Empty; sessionInfo.UserName = user?.Username; - sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user); + sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user); sessionInfo.RemoteEndPoint = remoteEndPoint; sessionInfo.Client = appName; @@ -483,7 +484,7 @@ namespace Emby.Server.Implementations.Session string deviceId, string deviceName, string remoteEndPoint, - Jellyfin.Data.Entities.User user) + User user) { var sessionInfo = new SessionInfo(this, _logger) { @@ -497,7 +498,7 @@ namespace Emby.Server.Implementations.Session sessionInfo.UserId = user?.Id ?? Guid.Empty; sessionInfo.UserName = username; - sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user); + sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user); sessionInfo.RemoteEndPoint = remoteEndPoint; if (string.IsNullOrEmpty(deviceName)) @@ -520,9 +521,9 @@ namespace Emby.Server.Implementations.Session return sessionInfo; } - private List GetUsers(SessionInfo session) + private List GetUsers(SessionInfo session) { - var users = new List(); + var users = new List(); if (session.UserId != Guid.Empty) { @@ -680,7 +681,7 @@ namespace Emby.Server.Implementations.Session /// /// The user object. /// The item. - private void OnPlaybackStart(Jellyfin.Data.Entities.User user, BaseItem item) + private void OnPlaybackStart(User user, BaseItem item) { var data = _userDataManager.GetUserData(user, item); @@ -763,7 +764,7 @@ namespace Emby.Server.Implementations.Session StartIdleCheckTimer(); } - private void OnPlaybackProgress(Jellyfin.Data.Entities.User user, BaseItem item, PlaybackProgressInfo info) + private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info) { var data = _userDataManager.GetUserData(user, item); @@ -789,7 +790,7 @@ namespace Emby.Server.Implementations.Session } } - private static bool UpdatePlaybackSettings(Jellyfin.Data.Entities.User user, PlaybackProgressInfo info, UserItemData data) + private static bool UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data) { var changed = false; @@ -949,7 +950,7 @@ namespace Emby.Server.Implementations.Session _logger); } - private bool OnPlaybackStopped(Jellyfin.Data.Entities.User user, BaseItem item, long? positionTicks, bool playbackFailed) + private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed) { bool playedToCompletion = false; @@ -1163,7 +1164,7 @@ namespace Emby.Server.Implementations.Session await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false); } - private IEnumerable TranslateItemForPlayback(Guid id, Jellyfin.Data.Entities.User user) + private IEnumerable TranslateItemForPlayback(Guid id, User user) { var item = _libraryManager.GetItemById(id); @@ -1216,7 +1217,7 @@ namespace Emby.Server.Implementations.Session return new[] { item }; } - private IEnumerable TranslateItemForInstantMix(Guid id, Jellyfin.Data.Entities.User user) + private IEnumerable TranslateItemForInstantMix(Guid id, User user) { var item = _libraryManager.GetItemById(id); @@ -1399,7 +1400,7 @@ namespace Emby.Server.Implementations.Session { CheckDisposed(); - Jellyfin.Data.Entities.User user = null; + User user = null; if (request.UserId != Guid.Empty) { user = _userManager.GetUserById(request.UserId); @@ -1455,7 +1456,7 @@ namespace Emby.Server.Implementations.Session return returnResult; } - private string GetAuthorizationToken(Jellyfin.Data.Entities.User user, string deviceId, string app, string appVersion, string deviceName) + private string GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName) { var existing = _authRepo.Get( new AuthenticationInfoQuery @@ -1701,7 +1702,7 @@ namespace Emby.Server.Implementations.Session return info; } - private string GetImageCacheTag(Jellyfin.Data.Entities.User user) + private string GetImageCacheTag(User user) { try { diff --git a/Jellyfin.Data/Entities/AccessSchedule.cs b/Jellyfin.Data/Entities/AccessSchedule.cs index 7966cdb50..711e94dd1 100644 --- a/Jellyfin.Data/Entities/AccessSchedule.cs +++ b/Jellyfin.Data/Entities/AccessSchedule.cs @@ -1,5 +1,7 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; using Jellyfin.Data.Enums; namespace Jellyfin.Data.Entities @@ -20,8 +22,9 @@ namespace Jellyfin.Data.Entities /// The day of the week. /// The start hour. /// The end hour. - public AccessSchedule(DynamicDayOfWeek dayOfWeek, double startHour, double endHour) + public AccessSchedule(DynamicDayOfWeek dayOfWeek, double startHour, double endHour, Guid userId) { + UserId = userId; DayOfWeek = dayOfWeek; StartHour = startHour; EndHour = endHour; @@ -34,15 +37,20 @@ namespace Jellyfin.Data.Entities /// The start hour. /// The end hour. /// The newly created instance. - public static AccessSchedule CreateInstance(DynamicDayOfWeek dayOfWeek, double startHour, double endHour) + public static AccessSchedule CreateInstance(DynamicDayOfWeek dayOfWeek, double startHour, double endHour, Guid userId) { - return new AccessSchedule(dayOfWeek, startHour, endHour); + return new AccessSchedule(dayOfWeek, startHour, endHour, userId); } + [JsonIgnore] [Key] [Required] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; set; } + + [Required] + [ForeignKey("Id")] + public Guid UserId { get; set; } /// /// Gets or sets the day of week. diff --git a/Jellyfin.Data/Entities/Group.cs b/Jellyfin.Data/Entities/Group.cs index 54f9f4905..017fb2234 100644 --- a/Jellyfin.Data/Entities/Group.cs +++ b/Jellyfin.Data/Entities/Group.cs @@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations.Schema; namespace Jellyfin.Data.Entities { - public partial class Group + public partial class Group : IHasPermissions, ISavingChanges { partial void Init(); @@ -14,35 +14,29 @@ namespace Jellyfin.Data.Entities /// protected Group() { - GroupPermissions = new HashSet(); + Permissions = new HashSet(); ProviderMappings = new HashSet(); Preferences = new HashSet(); Init(); } - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static Group CreateGroupUnsafe() - { - return new Group(); - } - /// /// Public constructor with required data /// /// - /// - public Group(string name, User _user0) + /// + public Group(string name, User user) { - if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); - this.Name = name; + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } - if (_user0 == null) throw new ArgumentNullException(nameof(_user0)); - _user0.Groups.Add(this); + this.Name = name; + user.Groups.Add(this); - this.GroupPermissions = new HashSet(); + this.Permissions = new HashSet(); this.ProviderMappings = new HashSet(); this.Preferences = new HashSet(); @@ -54,9 +48,9 @@ namespace Jellyfin.Data.Entities /// /// /// - public static Group Create(string name, User _user0) + public static Group Create(string name, User user) { - return new Group(name, _user0); + return new Group(name, user); } /************************************************************************* @@ -68,8 +62,7 @@ namespace Jellyfin.Data.Entities /// [Key] [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public Guid Id { get; protected set; } /// /// Required, Max length = 255 @@ -96,13 +89,13 @@ namespace Jellyfin.Data.Entities *************************************************************************/ [ForeignKey("Permission_GroupPermissions_Id")] - public virtual ICollection GroupPermissions { get; protected set; } + public ICollection Permissions { get; protected set; } [ForeignKey("ProviderMapping_ProviderMappings_Id")] - public virtual ICollection ProviderMappings { get; protected set; } + public ICollection ProviderMappings { get; protected set; } [ForeignKey("Preference_Preferences_Id")] - public virtual ICollection Preferences { get; protected set; } + public ICollection Preferences { get; protected set; } } } diff --git a/Jellyfin.Data/Entities/Permission.cs b/Jellyfin.Data/Entities/Permission.cs index 0b5b52cbd..136e7abd3 100644 --- a/Jellyfin.Data/Entities/Permission.cs +++ b/Jellyfin.Data/Entities/Permission.cs @@ -3,10 +3,11 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Runtime.CompilerServices; +using Jellyfin.Data.Enums; namespace Jellyfin.Data.Entities { - public partial class Permission + public partial class Permission : ISavingChanges { partial void Init(); @@ -18,33 +19,16 @@ namespace Jellyfin.Data.Entities Init(); } - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static Permission CreatePermissionUnsafe() - { - return new Permission(); - } - /// /// Public constructor with required data /// /// /// - /// - /// - public Permission(Enums.PermissionKind kind, bool value, User _user0, Group _group1) + /// + public Permission(PermissionKind kind, bool value) { - this.Kind = kind; - - this.Value = value; - - if (_user0 == null) throw new ArgumentNullException(nameof(_user0)); - _user0.Permissions.Add(this); - - if (_group1 == null) throw new ArgumentNullException(nameof(_group1)); - _group1.GroupPermissions.Add(this); - + Kind = kind; + Value = value; Init(); } @@ -54,11 +38,10 @@ namespace Jellyfin.Data.Entities /// /// /// - /// - /// - public static Permission Create(Enums.PermissionKind kind, bool value, User _user0, Group _group1) + /// + public static Permission Create(PermissionKind kind, bool value) { - return new Permission(kind, value, _user0, _group1); + return new Permission(kind, value); } /************************************************************************* @@ -76,31 +59,32 @@ namespace Jellyfin.Data.Entities /// /// Backing field for Kind /// - protected Enums.PermissionKind _Kind; + protected PermissionKind _Kind; /// /// When provided in a partial class, allows value of Kind to be changed before setting. /// - partial void SetKind(Enums.PermissionKind oldValue, ref Enums.PermissionKind newValue); + partial void SetKind(PermissionKind oldValue, ref PermissionKind newValue); /// /// When provided in a partial class, allows value of Kind to be changed before returning. /// - partial void GetKind(ref Enums.PermissionKind result); + partial void GetKind(ref PermissionKind result); /// /// Required /// [Required] - public Enums.PermissionKind Kind + public PermissionKind Kind { get { - Enums.PermissionKind value = _Kind; + PermissionKind value = _Kind; GetKind(ref value); - return (_Kind = value); + return _Kind = value; } + set { - Enums.PermissionKind oldValue = _Kind; + PermissionKind oldValue = _Kind; SetKind(oldValue, ref value); if (oldValue != value) { @@ -117,7 +101,7 @@ namespace Jellyfin.Data.Entities public bool Value { get; set; } /// - /// Required, ConcurrenyToken + /// Required, ConcurrencyToken. /// [ConcurrencyCheck] [Required] @@ -138,7 +122,6 @@ namespace Jellyfin.Data.Entities { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } - } } diff --git a/Jellyfin.Data/Entities/Preference.cs b/Jellyfin.Data/Entities/Preference.cs index 505f52e6b..56a07d440 100644 --- a/Jellyfin.Data/Entities/Preference.cs +++ b/Jellyfin.Data/Entities/Preference.cs @@ -1,63 +1,33 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Enums; namespace Jellyfin.Data.Entities { - public partial class Preference + /// + /// An entity representing a preference attached to a user or group. + /// + public class Preference : ISavingChanges { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected Preference() - { - Init(); - } - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// Initializes a new instance of the class. + /// Public constructor with required data. /// - public static Preference CreatePreferenceUnsafe() + /// The preference kind. + /// The value. + public Preference(PreferenceKind kind, string value) { - return new Preference(); + Kind = kind; + Value = value ?? throw new ArgumentNullException(nameof(value)); } /// - /// Public constructor with required data - /// - /// - /// - /// - /// - public Preference(Enums.PreferenceKind kind, string value, User _user0, Group _group1) - { - this.Kind = kind; - - if (string.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(value)); - this.Value = value; - - if (_user0 == null) throw new ArgumentNullException(nameof(_user0)); - _user0.Preferences.Add(this); - - if (_group1 == null) throw new ArgumentNullException(nameof(_group1)); - _group1.Preferences.Add(this); - - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) + /// Initializes a new instance of the class. + /// Default constructor. Protected due to required properties, but present because EF needs it. /// - /// - /// - /// - /// - public static Preference Create(Enums.PreferenceKind kind, string value, User _user0, Group _group1) + protected Preference() { - return new Preference(kind, value, _user0, _group1); } /************************************************************************* @@ -76,7 +46,7 @@ namespace Jellyfin.Data.Entities /// Required /// [Required] - public Enums.PreferenceKind Kind { get; set; } + public PreferenceKind Kind { get; set; } /// /// Required, Max length = 65535 @@ -87,21 +57,28 @@ namespace Jellyfin.Data.Entities public string Value { get; set; } /// - /// Required, ConcurrenyToken + /// Required, ConcurrencyToken. /// [ConcurrencyCheck] [Required] public uint RowVersion { get; set; } + /// + /// Static create function (for use in LINQ queries, etc.) + /// + /// The preference kind. + /// The value. + /// The new instance. + public static Preference Create(PreferenceKind kind, string value) + { + return new Preference(kind, value); + } + + /// public void OnSavingChanges() { RowVersion++; } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } } diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index 17913959e..7252ef230 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -9,45 +9,23 @@ using Jellyfin.Data.Enums; namespace Jellyfin.Data.Entities { - public partial class User + /// + /// An entity representing a user. + /// + public partial class User : IHasPermissions, ISavingChanges { /// /// The values being delimited here are Guids, so commas work as they do not appear in Guids. /// private const char Delimiter = ','; - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected User() - { - Groups = new HashSet(); - Permissions = new HashSet(); - ProviderMappings = new HashSet(); - Preferences = new HashSet(); - AccessSchedules = new HashSet(); - - Init(); - } - /// - /// Public constructor with required data + /// Initializes a new instance of the class. + /// Public constructor with required data. /// - /// - /// - /// - /// - /// - /// - public User( - string username, - bool mustUpdatePassword, - string authenticationProviderId, - int invalidLoginAttemptCount, - SubtitlePlaybackMode subtitleMode, - bool playDefaultAudioTrack) + /// The username for the new user. + /// The authentication provider's Id + public User(string username, string authenticationProviderId) { if (string.IsNullOrEmpty(username)) { @@ -60,11 +38,7 @@ namespace Jellyfin.Data.Entities } Username = username; - MustUpdatePassword = mustUpdatePassword; AuthenticationProviderId = authenticationProviderId; - InvalidLoginAttemptCount = invalidLoginAttemptCount; - SubtitleMode = subtitleMode; - PlayDefaultAudioTrack = playDefaultAudioTrack; Groups = new HashSet(); Permissions = new HashSet(); @@ -74,6 +48,8 @@ namespace Jellyfin.Data.Entities // Set default values Id = Guid.NewGuid(); + InvalidLoginAttemptCount = 0; + MustUpdatePassword = false; DisplayMissingEpisodes = false; DisplayCollectionsView = false; HidePlayedInLatest = true; @@ -81,36 +57,40 @@ namespace Jellyfin.Data.Entities RememberSubtitleSelections = true; EnableNextEpisodeAutoPlay = true; EnableAutoLogin = false; + PlayDefaultAudioTrack = true; + SubtitleMode = SubtitlePlaybackMode.Default; + AddDefaultPermissions(); + AddDefaultPreferences(); Init(); } /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// Initializes a new instance of the class. + /// Default constructor. Protected due to required properties, but present because EF needs it. /// - public static User CreateUserUnsafe() + protected User() { - return new User(); + Groups = new HashSet(); + Permissions = new HashSet(); + ProviderMappings = new HashSet(); + Preferences = new HashSet(); + AccessSchedules = new HashSet(); + + AddDefaultPermissions(); + AddDefaultPreferences(); + Init(); } /// /// Static create function (for use in LINQ queries, etc.) /// - /// - /// - /// - /// - /// - /// - public static User Create( - string username, - bool mustUpdatePassword, - string authenticationProviderId, - int invalidLoginAttemptCount, - SubtitlePlaybackMode subtitleMode, - bool playDefaultAudioTrack) + /// The username for the created user. + /// The Id of the user's authentication provider. + /// The created instance. + public static User Create(string username, string authenticationProviderId) { - return new User(username, mustUpdatePassword, authenticationProviderId, invalidLoginAttemptCount, subtitleMode, playDefaultAudioTrack); + return new User(username, authenticationProviderId); } /************************************************************************* @@ -131,7 +111,6 @@ namespace Jellyfin.Data.Entities [Required] [MaxLength(255)] [StringLength(255)] - [JsonPropertyName("Name")] public string Username { get; set; } /// @@ -199,6 +178,7 @@ namespace Jellyfin.Data.Entities public bool PlayDefaultAudioTrack { get; set; } /// + /// Gets or sets the subtitle language preference. /// Max length = 255 /// [MaxLength(255)] @@ -237,6 +217,7 @@ namespace Jellyfin.Data.Entities public int? RemoteClientBitrateLimit { get; set; } /// + /// Gets or sets the internal id. /// This is a temporary stopgap for until the library db is migrated. /// This corresponds to the value of the index of this user in the library db. /// @@ -246,7 +227,8 @@ namespace Jellyfin.Data.Entities public ImageInfo ProfileImage { get; set; } /// - /// Required, ConcurrenyToken + /// Gets or sets the row version. + /// Required, ConcurrenyToken. /// [ConcurrencyCheck] [Required] @@ -260,23 +242,25 @@ namespace Jellyfin.Data.Entities /************************************************************************* * Navigation properties *************************************************************************/ - [ForeignKey("Group_Groups_Id")] + [ForeignKey("Group_Groups_Guid")] public ICollection Groups { get; protected set; } - [ForeignKey("Permission_Permissions_Id")] + [ForeignKey("Permission_Permissions_Guid")] public ICollection Permissions { get; protected set; } [ForeignKey("ProviderMapping_ProviderMappings_Id")] public ICollection ProviderMappings { get; protected set; } - [ForeignKey("Preference_Preferences_Id")] + [ForeignKey("Preference_Preferences_Guid")] public ICollection Preferences { get; protected set; } public ICollection AccessSchedules { get; protected set; } + partial void Init(); + public bool HasPermission(PermissionKind permission) { - return Permissions.Select(p => p.Kind).Contains(permission); + return Permissions.First(p => p.Kind == permission).Value; } public void SetPermission(PermissionKind kind, bool value) @@ -287,11 +271,12 @@ namespace Jellyfin.Data.Entities public string[] GetPreference(PreferenceKind preference) { - return Preferences + var val = Preferences .Where(p => p.Kind == preference) .Select(p => p.Value) - .First() - .Split(Delimiter); + .First(); + + return Equals(val, string.Empty) ? Array.Empty() : val.Split(Delimiter); } public void SetPreference(PreferenceKind preference, string[] values) @@ -332,5 +317,39 @@ namespace Jellyfin.Data.Entities return hour >= schedule.StartHour && hour <= schedule.EndHour; } + + // TODO: make these user configurable? + private void AddDefaultPermissions() + { + Permissions.Add(new Permission(PermissionKind.IsAdministrator, false)); + Permissions.Add(new Permission(PermissionKind.IsDisabled, false)); + Permissions.Add(new Permission(PermissionKind.IsHidden, false)); + Permissions.Add(new Permission(PermissionKind.EnableAllChannels, false)); + Permissions.Add(new Permission(PermissionKind.EnableAllDevices, true)); + Permissions.Add(new Permission(PermissionKind.EnableAllFolders, false)); + Permissions.Add(new Permission(PermissionKind.EnableContentDeletion, false)); + Permissions.Add(new Permission(PermissionKind.EnableContentDownloading, true)); + Permissions.Add(new Permission(PermissionKind.EnableMediaConversion, true)); + Permissions.Add(new Permission(PermissionKind.EnableMediaPlayback, true)); + Permissions.Add(new Permission(PermissionKind.EnablePlaybackRemuxing, true)); + Permissions.Add(new Permission(PermissionKind.EnablePublicSharing, true)); + Permissions.Add(new Permission(PermissionKind.EnableRemoteAccess, true)); + Permissions.Add(new Permission(PermissionKind.EnableSyncTranscoding, true)); + Permissions.Add(new Permission(PermissionKind.EnableAudioPlaybackTranscoding, true)); + Permissions.Add(new Permission(PermissionKind.EnableLiveTvAccess, true)); + Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, true)); + Permissions.Add(new Permission(PermissionKind.EnableSharedDeviceControl, true)); + Permissions.Add(new Permission(PermissionKind.EnableVideoPlaybackTranscoding, true)); + Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false)); + Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false)); + } + + private void AddDefaultPreferences() + { + foreach (var val in Enum.GetValues(typeof(PreferenceKind)).Cast()) + { + Preferences.Add(new Preference(val, string.Empty)); + } + } } } diff --git a/Jellyfin.Data/IHasPermissions.cs b/Jellyfin.Data/IHasPermissions.cs new file mode 100644 index 000000000..a77e51e1e --- /dev/null +++ b/Jellyfin.Data/IHasPermissions.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Jellyfin.Data.Entities; + +namespace Jellyfin.Data +{ + public interface IHasPermissions + { + ICollection Permissions { get; } + } +} diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index 9ac97a131..8eb35ec87 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -16,6 +16,13 @@ namespace Jellyfin.Server.Implementations public partial class JellyfinDb : DbContext { public virtual DbSet ActivityLogs { get; set; } + + public virtual DbSet Groups { get; set; } + + public virtual DbSet Permissions { get; set; } + + public virtual DbSet Preferences { get; set; } + public virtual DbSet Users { get; set; } /*public virtual DbSet Artwork { get; set; } public virtual DbSet Books { get; set; } @@ -30,7 +37,6 @@ namespace Jellyfin.Server.Implementations public virtual DbSet Episodes { get; set; } public virtual DbSet EpisodeMetadata { get; set; } public virtual DbSet Genres { get; set; } - public virtual DbSet Groups { get; set; } public virtual DbSet Libraries { get; set; } public virtual DbSet LibraryItems { get; set; } public virtual DbSet LibraryRoot { get; set; } @@ -43,12 +49,10 @@ namespace Jellyfin.Server.Implementations public virtual DbSet MovieMetadata { get; set; } public virtual DbSet MusicAlbums { get; set; } public virtual DbSet MusicAlbumMetadata { get; set; } - public virtual DbSet Permissions { get; set; } public virtual DbSet People { get; set; } public virtual DbSet PersonRoles { get; set; } public virtual DbSet Photo { get; set; } public virtual DbSet PhotoMetadata { get; set; } - public virtual DbSet Preferences { get; set; } public virtual DbSet ProviderMappings { get; set; } public virtual DbSet Ratings { get; set; } diff --git a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs index eab531d38..8f5c19900 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs +++ b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs @@ -18,7 +18,7 @@ namespace Jellyfin.Server.Implementations public JellyfinDbProvider(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; - serviceProvider.GetService().Database.Migrate(); + serviceProvider.GetRequiredService().Database.Migrate(); } /// diff --git a/Jellyfin.Server.Implementations/Migrations/20200504195702_UserSchema.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200504195702_UserSchema.Designer.cs deleted file mode 100644 index 8313c6a3b..000000000 --- a/Jellyfin.Server.Implementations/Migrations/20200504195702_UserSchema.Designer.cs +++ /dev/null @@ -1,324 +0,0 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1601 - -// -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("20200504195702_UserSchema")] - partial class UserSchema - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasDefaultSchema("jellyfin") - .HasAnnotation("ProductVersion", "3.1.3"); - - modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DateCreated") - .HasColumnType("TEXT"); - - b.Property("ItemId") - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.Property("LogSeverity") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(512); - - b.Property("Overview") - .HasColumnType("TEXT") - .HasMaxLength(512); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ShortOverview") - .HasColumnType("TEXT") - .HasMaxLength(512); - - b.Property("Type") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("ActivityLog"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Group_Groups_Id") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(255); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("Group_Groups_Id"); - - b.ToTable("Group"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("Permission_GroupPermissions_Id") - .HasColumnType("INTEGER"); - - b.Property("Permission_Permissions_Id") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("Permission_GroupPermissions_Id"); - - b.HasIndex("Permission_Permissions_Id"); - - b.ToTable("Permission"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("Preference_Preferences_Id") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("Value") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(65535); - - b.HasKey("Id"); - - b.HasIndex("Preference_Preferences_Id"); - - b.ToTable("Preference"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ProviderMapping", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ProviderData") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(65535); - - b.Property("ProviderMapping_ProviderMappings_Id") - .HasColumnType("INTEGER"); - - b.Property("ProviderName") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(255); - - b.Property("ProviderSecrets") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(65535); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ProviderMapping_ProviderMappings_Id"); - - b.ToTable("ProviderMapping"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AudioLanguagePreference") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(255); - - b.Property("AuthenticationProviderId") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(255); - - b.Property("DisplayCollectionsView") - .HasColumnType("INTEGER"); - - b.Property("DisplayMissingEpisodes") - .HasColumnType("INTEGER"); - - b.Property("EnableNextEpisodeAutoPlay") - .HasColumnType("INTEGER"); - - b.Property("EnableUserPreferenceAccess") - .HasColumnType("INTEGER"); - - b.Property("GroupedFolders") - .HasColumnType("TEXT") - .HasMaxLength(65535); - - b.Property("HidePlayedInLatest") - .HasColumnType("INTEGER"); - - b.Property("InvalidLoginAttemptCount") - .HasColumnType("INTEGER"); - - b.Property("LatestItemExcludes") - .HasColumnType("TEXT") - .HasMaxLength(65535); - - b.Property("LoginAttemptsBeforeLockout") - .HasColumnType("INTEGER"); - - b.Property("MustUpdatePassword") - .HasColumnType("INTEGER"); - - b.Property("MyMediaExcludes") - .HasColumnType("TEXT") - .HasMaxLength(65535); - - b.Property("OrderedViews") - .HasColumnType("TEXT") - .HasMaxLength(65535); - - b.Property("Password") - .HasColumnType("TEXT") - .HasMaxLength(65535); - - b.Property("PlayDefaultAudioTrack") - .HasColumnType("INTEGER"); - - b.Property("RememberAudioSelections") - .HasColumnType("INTEGER"); - - b.Property("RememberSubtitleSelections") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SubtitleLanguagePrefernce") - .HasColumnType("TEXT") - .HasMaxLength(255); - - b.Property("SubtitleMode") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(255); - - b.Property("Username") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(255); - - b.HasKey("Id"); - - b.ToTable("User"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Group", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("Groups") - .HasForeignKey("Group_Groups_Id"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => - { - b.HasOne("Jellyfin.Data.Entities.Group", null) - .WithMany("GroupPermissions") - .HasForeignKey("Permission_GroupPermissions_Id"); - - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("Permissions") - .HasForeignKey("Permission_Permissions_Id"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => - { - b.HasOne("Jellyfin.Data.Entities.Group", null) - .WithMany("Preferences") - .HasForeignKey("Preference_Preferences_Id"); - - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("Preferences") - .HasForeignKey("Preference_Preferences_Id"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ProviderMapping", b => - { - b.HasOne("Jellyfin.Data.Entities.Group", null) - .WithMany("ProviderMappings") - .HasForeignKey("ProviderMapping_ProviderMappings_Id"); - - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("ProviderMappings") - .HasForeignKey("ProviderMapping_ProviderMappings_Id"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Jellyfin.Server.Implementations/Migrations/20200504195702_UserSchema.cs b/Jellyfin.Server.Implementations/Migrations/20200504195702_UserSchema.cs deleted file mode 100644 index f24ccccbf..000000000 --- a/Jellyfin.Server.Implementations/Migrations/20200504195702_UserSchema.cs +++ /dev/null @@ -1,219 +0,0 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1601 - -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Jellyfin.Server.Implementations.Migrations -{ - public partial class UserSchema : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "User", - schema: "jellyfin", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Username = table.Column(maxLength: 255, nullable: false), - Password = table.Column(maxLength: 65535, nullable: true), - MustUpdatePassword = table.Column(nullable: false), - AudioLanguagePreference = table.Column(maxLength: 255, nullable: false), - AuthenticationProviderId = table.Column(maxLength: 255, nullable: false), - GroupedFolders = table.Column(maxLength: 65535, nullable: true), - InvalidLoginAttemptCount = table.Column(nullable: false), - LatestItemExcludes = table.Column(maxLength: 65535, nullable: true), - LoginAttemptsBeforeLockout = table.Column(nullable: true), - MyMediaExcludes = table.Column(maxLength: 65535, nullable: true), - OrderedViews = table.Column(maxLength: 65535, nullable: true), - SubtitleMode = table.Column(maxLength: 255, nullable: false), - PlayDefaultAudioTrack = table.Column(nullable: false), - SubtitleLanguagePrefernce = table.Column(maxLength: 255, nullable: true), - DisplayMissingEpisodes = table.Column(nullable: true), - DisplayCollectionsView = table.Column(nullable: true), - HidePlayedInLatest = table.Column(nullable: true), - RememberAudioSelections = table.Column(nullable: true), - RememberSubtitleSelections = table.Column(nullable: true), - EnableNextEpisodeAutoPlay = table.Column(nullable: true), - EnableUserPreferenceAccess = table.Column(nullable: true), - RowVersion = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_User", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Group", - schema: "jellyfin", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(maxLength: 255, nullable: false), - RowVersion = table.Column(nullable: false), - Group_Groups_Id = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Group", x => x.Id); - table.ForeignKey( - name: "FK_Group_User_Group_Groups_Id", - column: x => x.Group_Groups_Id, - principalSchema: "jellyfin", - principalTable: "User", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "Permission", - schema: "jellyfin", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Kind = table.Column(nullable: false), - Value = table.Column(nullable: false), - RowVersion = table.Column(nullable: false), - Permission_GroupPermissions_Id = table.Column(nullable: true), - Permission_Permissions_Id = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Permission", x => x.Id); - table.ForeignKey( - name: "FK_Permission_Group_Permission_GroupPermissions_Id", - column: x => x.Permission_GroupPermissions_Id, - principalSchema: "jellyfin", - principalTable: "Group", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_Permission_User_Permission_Permissions_Id", - column: x => x.Permission_Permissions_Id, - principalSchema: "jellyfin", - principalTable: "User", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "Preference", - schema: "jellyfin", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Kind = table.Column(nullable: false), - Value = table.Column(maxLength: 65535, nullable: false), - RowVersion = table.Column(nullable: false), - Preference_Preferences_Id = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Preference", x => x.Id); - table.ForeignKey( - name: "FK_Preference_Group_Preference_Preferences_Id", - column: x => x.Preference_Preferences_Id, - principalSchema: "jellyfin", - principalTable: "Group", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_Preference_User_Preference_Preferences_Id", - column: x => x.Preference_Preferences_Id, - principalSchema: "jellyfin", - principalTable: "User", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "ProviderMapping", - schema: "jellyfin", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - ProviderName = table.Column(maxLength: 255, nullable: false), - ProviderSecrets = table.Column(maxLength: 65535, nullable: false), - ProviderData = table.Column(maxLength: 65535, nullable: false), - RowVersion = table.Column(nullable: false), - ProviderMapping_ProviderMappings_Id = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_ProviderMapping", x => x.Id); - table.ForeignKey( - name: "FK_ProviderMapping_Group_ProviderMapping_ProviderMappings_Id", - column: x => x.ProviderMapping_ProviderMappings_Id, - principalSchema: "jellyfin", - principalTable: "Group", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_ProviderMapping_User_ProviderMapping_ProviderMappings_Id", - column: x => x.ProviderMapping_ProviderMappings_Id, - principalSchema: "jellyfin", - principalTable: "User", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateIndex( - name: "IX_Group_Group_Groups_Id", - schema: "jellyfin", - table: "Group", - column: "Group_Groups_Id"); - - migrationBuilder.CreateIndex( - name: "IX_Permission_Permission_GroupPermissions_Id", - schema: "jellyfin", - table: "Permission", - column: "Permission_GroupPermissions_Id"); - - migrationBuilder.CreateIndex( - name: "IX_Permission_Permission_Permissions_Id", - schema: "jellyfin", - table: "Permission", - column: "Permission_Permissions_Id"); - - migrationBuilder.CreateIndex( - name: "IX_Preference_Preference_Preferences_Id", - schema: "jellyfin", - table: "Preference", - column: "Preference_Preferences_Id"); - - migrationBuilder.CreateIndex( - name: "IX_ProviderMapping_ProviderMapping_ProviderMappings_Id", - schema: "jellyfin", - table: "ProviderMapping", - column: "ProviderMapping_ProviderMappings_Id"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Permission", - schema: "jellyfin"); - - migrationBuilder.DropTable( - name: "Preference", - schema: "jellyfin"); - - migrationBuilder.DropTable( - name: "ProviderMapping", - schema: "jellyfin"); - - migrationBuilder.DropTable( - name: "Group", - schema: "jellyfin"); - - migrationBuilder.DropTable( - name: "User", - schema: "jellyfin"); - } - } -} diff --git a/Jellyfin.Server.Implementations/Migrations/20200517002411_AddUsers.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200517002411_AddUsers.Designer.cs new file mode 100644 index 000000000..36c58c8ca --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20200517002411_AddUsers.Designer.cs @@ -0,0 +1,404 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1601 + +// +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("20200517002411_AddUsers")] + partial class AddUsers + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("jellyfin") + .HasAnnotation("ProductVersion", "3.1.3"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedule"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Overview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Group_Groups_Guid") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Group_Groups_Guid"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ImageInfo"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_GroupPermissions_Id") + .HasColumnType("TEXT"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Permission_GroupPermissions_Id"); + + b.HasIndex("Permission_Permissions_Guid"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("Preference_Preferences_Id") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.HasKey("Id"); + + b.HasIndex("Preference_Preferences_Guid"); + + b.HasIndex("Preference_Preferences_Id"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ProviderMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ProviderData") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("ProviderMapping_ProviderMappings_Id") + .HasColumnType("TEXT"); + + b.Property("ProviderName") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("ProviderSecrets") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderMapping_ProviderMappings_Id"); + + b.ToTable("ProviderMapping"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EasyPassword") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("ProfileImageId") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.HasIndex("ProfileImageId"); + + 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.Group", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Groups") + .HasForeignKey("Group_Groups_Guid"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.Group", null) + .WithMany("Permissions") + .HasForeignKey("Permission_GroupPermissions_Id"); + + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("Permission_Permissions_Guid"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("Preference_Preferences_Guid"); + + b.HasOne("Jellyfin.Data.Entities.Group", null) + .WithMany("Preferences") + .HasForeignKey("Preference_Preferences_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ProviderMapping", b => + { + b.HasOne("Jellyfin.Data.Entities.Group", null) + .WithMany("ProviderMappings") + .HasForeignKey("ProviderMapping_ProviderMappings_Id"); + + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ProviderMappings") + .HasForeignKey("ProviderMapping_ProviderMappings_Id"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.HasOne("Jellyfin.Data.Entities.ImageInfo", "ProfileImage") + .WithMany() + .HasForeignKey("ProfileImageId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20200517002411_AddUsers.cs b/Jellyfin.Server.Implementations/Migrations/20200517002411_AddUsers.cs new file mode 100644 index 000000000..55c6f371c --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20200517002411_AddUsers.cs @@ -0,0 +1,297 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1601 + +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Jellyfin.Server.Implementations.Migrations +{ + public partial class AddUsers : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ImageInfo", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Path = table.Column(nullable: false), + LastModified = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ImageInfo", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false), + Username = table.Column(maxLength: 255, nullable: false), + Password = table.Column(maxLength: 65535, nullable: true), + EasyPassword = table.Column(maxLength: 65535, nullable: true), + MustUpdatePassword = table.Column(nullable: false), + AudioLanguagePreference = table.Column(maxLength: 255, nullable: true), + AuthenticationProviderId = table.Column(maxLength: 255, nullable: false), + PasswordResetProviderId = table.Column(maxLength: 255, nullable: false), + InvalidLoginAttemptCount = table.Column(nullable: false), + LastActivityDate = table.Column(nullable: false), + LastLoginDate = table.Column(nullable: false), + LoginAttemptsBeforeLockout = table.Column(nullable: true), + SubtitleMode = table.Column(nullable: false), + PlayDefaultAudioTrack = table.Column(nullable: false), + SubtitleLanguagePreference = table.Column(maxLength: 255, nullable: true), + DisplayMissingEpisodes = table.Column(nullable: false), + DisplayCollectionsView = table.Column(nullable: false), + EnableLocalPassword = table.Column(nullable: false), + HidePlayedInLatest = table.Column(nullable: false), + RememberAudioSelections = table.Column(nullable: false), + RememberSubtitleSelections = table.Column(nullable: false), + EnableNextEpisodeAutoPlay = table.Column(nullable: false), + EnableAutoLogin = table.Column(nullable: false), + EnableUserPreferenceAccess = table.Column(nullable: false), + MaxParentalAgeRating = table.Column(nullable: true), + RemoteClientBitrateLimit = table.Column(nullable: true), + InternalId = table.Column(nullable: false), + ProfileImageId = table.Column(nullable: true), + RowVersion = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + table.ForeignKey( + name: "FK_Users_ImageInfo_ProfileImageId", + column: x => x.ProfileImageId, + principalSchema: "jellyfin", + principalTable: "ImageInfo", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "AccessSchedule", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(nullable: false), + DayOfWeek = table.Column(nullable: false), + StartHour = table.Column(nullable: false), + EndHour = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AccessSchedule", x => x.Id); + table.ForeignKey( + name: "FK_AccessSchedule_Users_UserId", + column: x => x.UserId, + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Groups", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false), + Name = table.Column(maxLength: 255, nullable: false), + RowVersion = table.Column(nullable: false), + Group_Groups_Guid = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Groups", x => x.Id); + table.ForeignKey( + name: "FK_Groups_Users_Group_Groups_Guid", + column: x => x.Group_Groups_Guid, + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Permissions", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Kind = table.Column(nullable: false), + Value = table.Column(nullable: false), + RowVersion = table.Column(nullable: false), + Permission_GroupPermissions_Id = table.Column(nullable: true), + Permission_Permissions_Guid = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Permissions", x => x.Id); + table.ForeignKey( + name: "FK_Permissions_Groups_Permission_GroupPermissions_Id", + column: x => x.Permission_GroupPermissions_Id, + principalSchema: "jellyfin", + principalTable: "Groups", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Permissions_Users_Permission_Permissions_Guid", + column: x => x.Permission_Permissions_Guid, + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Preferences", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Kind = table.Column(nullable: false), + Value = table.Column(maxLength: 65535, nullable: false), + RowVersion = table.Column(nullable: false), + Preference_Preferences_Guid = table.Column(nullable: true), + Preference_Preferences_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Preferences", x => x.Id); + table.ForeignKey( + name: "FK_Preferences_Users_Preference_Preferences_Guid", + column: x => x.Preference_Preferences_Guid, + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Preferences_Groups_Preference_Preferences_Id", + column: x => x.Preference_Preferences_Id, + principalSchema: "jellyfin", + principalTable: "Groups", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "ProviderMapping", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ProviderName = table.Column(maxLength: 255, nullable: false), + ProviderSecrets = table.Column(maxLength: 65535, nullable: false), + ProviderData = table.Column(maxLength: 65535, nullable: false), + RowVersion = table.Column(nullable: false), + ProviderMapping_ProviderMappings_Id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ProviderMapping", x => x.Id); + table.ForeignKey( + name: "FK_ProviderMapping_Groups_ProviderMapping_ProviderMappings_Id", + column: x => x.ProviderMapping_ProviderMappings_Id, + principalSchema: "jellyfin", + principalTable: "Groups", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_ProviderMapping_Users_ProviderMapping_ProviderMappings_Id", + column: x => x.ProviderMapping_ProviderMappings_Id, + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_AccessSchedule_UserId", + schema: "jellyfin", + table: "AccessSchedule", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Groups_Group_Groups_Guid", + schema: "jellyfin", + table: "Groups", + column: "Group_Groups_Guid"); + + migrationBuilder.CreateIndex( + name: "IX_Permissions_Permission_GroupPermissions_Id", + schema: "jellyfin", + table: "Permissions", + column: "Permission_GroupPermissions_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Permissions_Permission_Permissions_Guid", + schema: "jellyfin", + table: "Permissions", + column: "Permission_Permissions_Guid"); + + migrationBuilder.CreateIndex( + name: "IX_Preferences_Preference_Preferences_Guid", + schema: "jellyfin", + table: "Preferences", + column: "Preference_Preferences_Guid"); + + migrationBuilder.CreateIndex( + name: "IX_Preferences_Preference_Preferences_Id", + schema: "jellyfin", + table: "Preferences", + column: "Preference_Preferences_Id"); + + migrationBuilder.CreateIndex( + name: "IX_ProviderMapping_ProviderMapping_ProviderMappings_Id", + schema: "jellyfin", + table: "ProviderMapping", + column: "ProviderMapping_ProviderMappings_Id"); + + migrationBuilder.CreateIndex( + name: "IX_Users_ProfileImageId", + schema: "jellyfin", + table: "Users", + column: "ProfileImageId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AccessSchedule", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Permissions", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Preferences", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "ProviderMapping", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Groups", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "Users", + schema: "jellyfin"); + + migrationBuilder.DropTable( + name: "ImageInfo", + schema: "jellyfin"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index 0fb0ba803..46714e865 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -1,7 +1,9 @@ // using System; +using Jellyfin.Server.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Jellyfin.Server.Implementations.Migrations { @@ -15,6 +17,31 @@ namespace Jellyfin.Server.Implementations.Migrations .HasDefaultSchema("jellyfin") .HasAnnotation("ProductVersion", "3.1.3"); + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedule"); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => { b.Property("Id") @@ -63,12 +90,12 @@ namespace Jellyfin.Server.Implementations.Migrations modelBuilder.Entity("Jellyfin.Data.Entities.Group", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("TEXT"); - b.Property("Group_Groups_Id") - .HasColumnType("INTEGER"); + b.Property("Group_Groups_Guid") + .HasColumnType("TEXT"); b.Property("Name") .IsRequired() @@ -81,9 +108,27 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasKey("Id"); - b.HasIndex("Group_Groups_Id"); + b.HasIndex("Group_Groups_Guid"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); - b.ToTable("Group"); + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ImageInfo"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => @@ -95,11 +140,11 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("Kind") .HasColumnType("INTEGER"); - b.Property("Permission_GroupPermissions_Id") - .HasColumnType("INTEGER"); + b.Property("Permission_GroupPermissions_Id") + .HasColumnType("TEXT"); - b.Property("Permission_Permissions_Id") - .HasColumnType("INTEGER"); + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); b.Property("RowVersion") .IsConcurrencyToken() @@ -112,9 +157,9 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("Permission_GroupPermissions_Id"); - b.HasIndex("Permission_Permissions_Id"); + b.HasIndex("Permission_Permissions_Guid"); - b.ToTable("Permission"); + b.ToTable("Permissions"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => @@ -126,8 +171,11 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("Kind") .HasColumnType("INTEGER"); - b.Property("Preference_Preferences_Id") - .HasColumnType("INTEGER"); + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("Preference_Preferences_Id") + .HasColumnType("TEXT"); b.Property("RowVersion") .IsConcurrencyToken() @@ -140,9 +188,11 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasKey("Id"); + b.HasIndex("Preference_Preferences_Guid"); + b.HasIndex("Preference_Preferences_Id"); - b.ToTable("Preference"); + b.ToTable("Preferences"); }); modelBuilder.Entity("Jellyfin.Data.Entities.ProviderMapping", b => @@ -156,8 +206,8 @@ namespace Jellyfin.Server.Implementations.Migrations .HasColumnType("TEXT") .HasMaxLength(65535); - b.Property("ProviderMapping_ProviderMappings_Id") - .HasColumnType("INTEGER"); + b.Property("ProviderMapping_ProviderMappings_Id") + .HasColumnType("TEXT"); b.Property("ProviderName") .IsRequired() @@ -182,12 +232,11 @@ namespace Jellyfin.Server.Implementations.Migrations modelBuilder.Entity("Jellyfin.Data.Entities.User", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("TEXT"); b.Property("AudioLanguagePreference") - .IsRequired() .HasColumnType("TEXT") .HasMaxLength(255); @@ -196,71 +245,86 @@ namespace Jellyfin.Server.Implementations.Migrations .HasColumnType("TEXT") .HasMaxLength(255); - b.Property("DisplayCollectionsView") + b.Property("DisplayCollectionsView") .HasColumnType("INTEGER"); - b.Property("DisplayMissingEpisodes") + b.Property("DisplayMissingEpisodes") .HasColumnType("INTEGER"); - b.Property("EnableNextEpisodeAutoPlay") + b.Property("EasyPassword") + .HasColumnType("TEXT") + .HasMaxLength(65535); + + b.Property("EnableAutoLogin") .HasColumnType("INTEGER"); - b.Property("EnableUserPreferenceAccess") + b.Property("EnableLocalPassword") .HasColumnType("INTEGER"); - b.Property("GroupedFolders") - .HasColumnType("TEXT") - .HasMaxLength(65535); + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); - b.Property("HidePlayedInLatest") + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") .HasColumnType("INTEGER"); b.Property("InvalidLoginAttemptCount") .HasColumnType("INTEGER"); - b.Property("LatestItemExcludes") - .HasColumnType("TEXT") - .HasMaxLength(65535); + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); b.Property("LoginAttemptsBeforeLockout") .HasColumnType("INTEGER"); - b.Property("MustUpdatePassword") + b.Property("MaxParentalAgeRating") .HasColumnType("INTEGER"); - b.Property("MyMediaExcludes") - .HasColumnType("TEXT") - .HasMaxLength(65535); + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); - b.Property("OrderedViews") + b.Property("Password") .HasColumnType("TEXT") .HasMaxLength(65535); - b.Property("Password") + b.Property("PasswordResetProviderId") + .IsRequired() .HasColumnType("TEXT") - .HasMaxLength(65535); + .HasMaxLength(255); b.Property("PlayDefaultAudioTrack") .HasColumnType("INTEGER"); - b.Property("RememberAudioSelections") + b.Property("ProfileImageId") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") .HasColumnType("INTEGER"); - b.Property("RememberSubtitleSelections") + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") .HasColumnType("INTEGER"); b.Property("RowVersion") .IsConcurrencyToken() .HasColumnType("INTEGER"); - b.Property("SubtitleLanguagePrefernce") + b.Property("SubtitleLanguagePreference") .HasColumnType("TEXT") .HasMaxLength(255); - b.Property("SubtitleMode") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(255); + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); b.Property("Username") .IsRequired() @@ -269,34 +333,45 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasKey("Id"); - b.ToTable("User"); + b.HasIndex("ProfileImageId"); + + 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.Group", b => { b.HasOne("Jellyfin.Data.Entities.User", null) .WithMany("Groups") - .HasForeignKey("Group_Groups_Id"); + .HasForeignKey("Group_Groups_Guid"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => { b.HasOne("Jellyfin.Data.Entities.Group", null) - .WithMany("GroupPermissions") + .WithMany("Permissions") .HasForeignKey("Permission_GroupPermissions_Id"); b.HasOne("Jellyfin.Data.Entities.User", null) .WithMany("Permissions") - .HasForeignKey("Permission_Permissions_Id"); + .HasForeignKey("Permission_Permissions_Guid"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => { - b.HasOne("Jellyfin.Data.Entities.Group", null) + b.HasOne("Jellyfin.Data.Entities.User", null) .WithMany("Preferences") - .HasForeignKey("Preference_Preferences_Id"); + .HasForeignKey("Preference_Preferences_Guid"); - b.HasOne("Jellyfin.Data.Entities.User", null) + b.HasOne("Jellyfin.Data.Entities.Group", null) .WithMany("Preferences") .HasForeignKey("Preference_Preferences_Id"); }); @@ -311,6 +386,13 @@ namespace Jellyfin.Server.Implementations.Migrations .WithMany("ProviderMappings") .HasForeignKey("ProviderMapping_ProviderMappings_Id"); }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.HasOne("Jellyfin.Data.Entities.ImageInfo", "ProfileImage") + .WithMany() + .HasForeignKey("ProfileImageId"); + }); #pragma warning restore 612, 618 } } diff --git a/Jellyfin.Server.Implementations/User/DefaultAuthenticationProvider.cs b/Jellyfin.Server.Implementations/User/DefaultAuthenticationProvider.cs deleted file mode 100644 index 024500bf8..000000000 --- a/Jellyfin.Server.Implementations/User/DefaultAuthenticationProvider.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using MediaBrowser.Common; -using MediaBrowser.Common.Cryptography; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Model.Cryptography; - -namespace Jellyfin.Server.Implementations.User -{ - /// - /// The default authentication provider. - /// - public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser - { - private readonly ICryptoProvider _cryptographyProvider; - - /// - /// Initializes a new instance of the class. - /// - /// The cryptography provider. - public DefaultAuthenticationProvider(ICryptoProvider cryptographyProvider) - { - _cryptographyProvider = cryptographyProvider; - } - - /// - public string Name => "Default"; - - /// - public bool IsEnabled => true; - - /// - // This is dumb and an artifact of the backwards way auth providers were designed. - // This version of authenticate was never meant to be called, but needs to be here for interface compat - // Only the providers that don't provide local user support use this - public Task Authenticate(string username, string password) - { - throw new NotImplementedException(); - } - - /// - // This is the version that we need to use for local users. Because reasons. - public Task Authenticate(string username, string password, Data.Entities.User resolvedUser) - { - if (resolvedUser == null) - { - throw new AuthenticationException("Specified user does not exist."); - } - - bool success = false; - - // As long as jellyfin supports passwordless users, we need this little block here to accommodate - if (!HasPassword(resolvedUser)) - { - return Task.FromResult(new ProviderAuthenticationResult - { - Username = username - }); - } - - byte[] passwordBytes = Encoding.UTF8.GetBytes(password); - - PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password); - if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id) - || _cryptographyProvider.DefaultHashMethod == readyHash.Id) - { - byte[] calculatedHash = _cryptographyProvider.ComputeHash( - readyHash.Id, - passwordBytes, - readyHash.Salt.ToArray()); - - if (readyHash.Hash.SequenceEqual(calculatedHash)) - { - success = true; - } - } - else - { - throw new AuthenticationException($"Requested crypto method not available in provider: {readyHash.Id}"); - } - - if (!success) - { - throw new AuthenticationException("Invalid username or password"); - } - - return Task.FromResult(new ProviderAuthenticationResult - { - Username = username - }); - } - - /// - public bool HasPassword(Data.Entities.User user) - => !string.IsNullOrEmpty(user.Password); - - /// - public Task ChangePassword(Data.Entities.User user, string newPassword) - { - if (string.IsNullOrEmpty(newPassword)) - { - user.Password = null; - return Task.CompletedTask; - } - - PasswordHash newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword); - user.Password = newPasswordHash.ToString(); - - return Task.CompletedTask; - } - - /// - public void ChangeEasyPassword(Data.Entities.User user, string newPassword, string newPasswordHash) - { - if (newPassword != null) - { - newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword).ToString(); - } - - if (string.IsNullOrWhiteSpace(newPasswordHash)) - { - throw new ArgumentNullException(nameof(newPasswordHash)); - } - - user.EasyPassword = newPasswordHash; - } - - /// - public string GetEasyPasswordHash(Data.Entities.User user) - { - return string.IsNullOrEmpty(user.EasyPassword) - ? null - : Hex.Encode(PasswordHash.Parse(user.EasyPassword).Hash); - } - - /// - /// Hashes the provided string. - /// - /// The user. - /// The string to hash. - /// The hashed string. - public string GetHashedString(Data.Entities.User user, string str) - { - if (string.IsNullOrEmpty(user.Password)) - { - return _cryptographyProvider.CreatePasswordHash(str).ToString(); - } - - // TODO: make use of iterations parameter? - PasswordHash passwordHash = PasswordHash.Parse(user.Password); - var salt = passwordHash.Salt.ToArray(); - return new PasswordHash( - passwordHash.Id, - _cryptographyProvider.ComputeHash( - passwordHash.Id, - Encoding.UTF8.GetBytes(str), - salt), - salt, - passwordHash.Parameters.ToDictionary(x => x.Key, y => y.Value)).ToString(); - } - - /// - /// Hashes the provided string. - /// - /// The user. - /// The string to hash. - /// The hashed string. - public ReadOnlySpan GetHashed(Data.Entities.User user, string str) - { - if (string.IsNullOrEmpty(user.Password)) - { - return _cryptographyProvider.CreatePasswordHash(str).Hash; - } - - // TODO: make use of iterations parameter? - PasswordHash passwordHash = PasswordHash.Parse(user.Password); - return _cryptographyProvider.ComputeHash( - passwordHash.Id, - Encoding.UTF8.GetBytes(str), - passwordHash.Salt.ToArray()); - } - } -} diff --git a/Jellyfin.Server.Implementations/User/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/User/DefaultPasswordResetProvider.cs deleted file mode 100644 index 80ab3ce00..000000000 --- a/Jellyfin.Server.Implementations/User/DefaultPasswordResetProvider.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Security.Cryptography; -using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Users; - -namespace Jellyfin.Server.Implementations.User -{ - /// - /// The default password reset provider. - /// - public class DefaultPasswordResetProvider : IPasswordResetProvider - { - private const string BaseResetFileName = "passwordreset"; - - private readonly IJsonSerializer _jsonSerializer; - private readonly IUserManager _userManager; - - private readonly string _passwordResetFileBase; - private readonly string _passwordResetFileBaseDir; - - /// - /// Initializes a new instance of the class. - /// - /// The configuration manager. - /// The JSON serializer. - /// The user manager. - public DefaultPasswordResetProvider( - IServerConfigurationManager configurationManager, - IJsonSerializer jsonSerializer, - IUserManager userManager) - { - _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; - _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName); - _jsonSerializer = jsonSerializer; - _userManager = userManager; - } - - /// - public string Name => "Default Password Reset Provider"; - - /// - public bool IsEnabled => true; - - /// - public async Task RedeemPasswordResetPin(string pin) - { - SerializablePasswordReset spr; - List usersReset = new List(); - foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*")) - { - await using (var str = File.OpenRead(resetFile)) - { - spr = await _jsonSerializer.DeserializeFromStreamAsync(str).ConfigureAwait(false); - } - - if (spr.ExpirationDate < DateTime.Now) - { - File.Delete(resetFile); - } - else if (string.Equals( - spr.Pin.Replace("-", string.Empty, StringComparison.Ordinal), - pin.Replace("-", string.Empty, StringComparison.Ordinal), - StringComparison.InvariantCultureIgnoreCase)) - { - var resetUser = _userManager.GetUserByName(spr.UserName); - if (resetUser == null) - { - throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found"); - } - - await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); - usersReset.Add(resetUser.Username); - File.Delete(resetFile); - } - } - - if (usersReset.Count < 1) - { - throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}"); - } - - return new PinRedeemResult - { - Success = true, - UsersReset = usersReset.ToArray() - }; - } - - /// - public async Task StartForgotPasswordProcess(Jellyfin.Data.Entities.User user, bool isInNetwork) - { - string pin; - using (var cryptoRandom = RandomNumberGenerator.Create()) - { - byte[] bytes = new byte[4]; - cryptoRandom.GetBytes(bytes); - pin = BitConverter.ToString(bytes); - } - - DateTime expireTime = DateTime.Now.AddMinutes(30); - - user.EasyPassword = pin; - await _userManager.UpdateUserAsync(user).ConfigureAwait(false); - - return new ForgotPasswordResult - { - Action = ForgotPasswordAction.PinCode, - PinExpirationDate = expireTime, - }; - } - - private class SerializablePasswordReset : PasswordPinCreationResult - { - public string Pin { get; set; } - - public string UserName { get; set; } - } - } -} diff --git a/Jellyfin.Server.Implementations/User/DeviceAccessEntryPoint.cs b/Jellyfin.Server.Implementations/User/DeviceAccessEntryPoint.cs deleted file mode 100644 index d33034ab2..000000000 --- a/Jellyfin.Server.Implementations/User/DeviceAccessEntryPoint.cs +++ /dev/null @@ -1,65 +0,0 @@ -#pragma warning disable CS1591 - -using System.Threading.Tasks; -using Jellyfin.Data.Enums; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Security; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Events; - -namespace Jellyfin.Server.Implementations.User -{ - 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) - { - _userManager = userManager; - _authRepo = authRepo; - _deviceManager = deviceManager; - _sessionManager = sessionManager; - } - - public Task RunAsync() - { - _userManager.OnUserUpdated += OnUserUpdated; - - return Task.CompletedTask; - } - - private void OnUserUpdated(object sender, GenericEventArgs e) - { - var user = e.Argument; - if (!user.HasPermission(PermissionKind.EnableAllDevices)) - { - UpdateDeviceAccess(user); - } - } - - public void Dispose() - { - } - - private void UpdateDeviceAccess(Data.Entities.User user) - { - var existing = _authRepo.Get(new AuthenticationInfoQuery - { - UserId = user.Id - }).Items; - - foreach (var authInfo in existing) - { - if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId)) - { - _sessionManager.Logout(authInfo); - } - } - } - } -} diff --git a/Jellyfin.Server.Implementations/User/InvalidAuthProvider.cs b/Jellyfin.Server.Implementations/User/InvalidAuthProvider.cs deleted file mode 100644 index a11ca128a..000000000 --- a/Jellyfin.Server.Implementations/User/InvalidAuthProvider.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Controller.Authentication; - -namespace Jellyfin.Server.Implementations.User -{ - /// - /// An invalid authentication provider. - /// - public class InvalidAuthProvider : IAuthenticationProvider - { - /// - public string Name => "InvalidOrMissingAuthenticationProvider"; - - /// - public bool IsEnabled => true; - - /// - public Task Authenticate(string username, string password) - { - throw new AuthenticationException("User Account cannot login with this provider. The Normal provider for this user cannot be found"); - } - - /// - public bool HasPassword(Data.Entities.User user) - { - return true; - } - - /// - public Task ChangePassword(Data.Entities.User user, string newPassword) - { - return Task.CompletedTask; - } - - /// - public void ChangeEasyPassword(Data.Entities.User user, string newPassword, string newPasswordHash) - { - // Nothing here - } - - /// - public string GetEasyPasswordHash(Data.Entities.User user) - { - return string.Empty; - } - } -} diff --git a/Jellyfin.Server.Implementations/User/UserManager.cs b/Jellyfin.Server.Implementations/User/UserManager.cs deleted file mode 100644 index 73905ff70..000000000 --- a/Jellyfin.Server.Implementations/User/UserManager.cs +++ /dev/null @@ -1,771 +0,0 @@ -#pragma warning disable CS0067 -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Jellyfin.Data.Enums; -using MediaBrowser.Common.Cryptography; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Users; -using Microsoft.Extensions.Logging; - -namespace Jellyfin.Server.Implementations.User -{ - public class UserManager : IUserManager - { - private readonly JellyfinDbProvider _dbProvider; - private readonly ICryptoProvider _cryptoProvider; - private readonly INetworkManager _networkManager; - private readonly ILogger _logger; - - private IAuthenticationProvider[] _authenticationProviders; - private DefaultAuthenticationProvider _defaultAuthenticationProvider; - private InvalidAuthProvider _invalidAuthProvider; - private IPasswordResetProvider[] _passwordResetProviders; - private DefaultPasswordResetProvider _defaultPasswordResetProvider; - - public UserManager( - JellyfinDbProvider dbProvider, - ICryptoProvider cryptoProvider, - INetworkManager networkManager, - ILogger logger) - { - _dbProvider = dbProvider; - _cryptoProvider = cryptoProvider; - _networkManager = networkManager; - _logger = logger; - } - - public event EventHandler> OnUserPasswordChanged; - - /// - public event EventHandler> OnUserUpdated; - - /// - public event EventHandler> OnUserCreated; - - /// - public event EventHandler> OnUserDeleted; - - public event EventHandler> OnUserLockedOut; - - public IEnumerable Users - { - get - { - using var dbContext = _dbProvider.CreateContext(); - return dbContext.Users; - } - } - - public IEnumerable UsersIds - { - get - { - using var dbContext = _dbProvider.CreateContext(); - return dbContext.Users.Select(u => u.Id); - } - } - - public Data.Entities.User GetUserById(Guid id) - { - if (id == Guid.Empty) - { - throw new ArgumentException("Guid can't be empty", nameof(id)); - } - - using var dbContext = _dbProvider.CreateContext(); - - return dbContext.Users.Find(id); - } - - public Data.Entities.User GetUserByName(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException("Invalid username", nameof(name)); - } - - using var dbContext = _dbProvider.CreateContext(); - - return dbContext.Users.FirstOrDefault(u => - string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase)); - } - - public async Task RenameUser(Data.Entities.User user, string newName) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (string.IsNullOrWhiteSpace(newName)) - { - throw new ArgumentException("Invalid username", nameof(newName)); - } - - if (user.Username.Equals(newName, StringComparison.Ordinal)) - { - throw new ArgumentException("The new and old names must be different."); - } - - if (Users.Any( - u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.OrdinalIgnoreCase))) - { - throw new ArgumentException(string.Format( - CultureInfo.InvariantCulture, - "A user with the name '{0}' already exists.", - newName)); - } - - user.Username = newName; - await UpdateUserAsync(user).ConfigureAwait(false); - - OnUserUpdated?.Invoke(this, new GenericEventArgs(user)); - } - - public void UpdateUser(Data.Entities.User user) - { - using var dbContext = _dbProvider.CreateContext(); - dbContext.Users.Update(user); - dbContext.SaveChanges(); - } - - public async Task UpdateUserAsync(Data.Entities.User user) - { - await using var dbContext = _dbProvider.CreateContext(); - dbContext.Users.Update(user); - - await dbContext.SaveChangesAsync().ConfigureAwait(false); - } - - public Data.Entities.User CreateUser(string name) - { - using var dbContext = _dbProvider.CreateContext(); - - var newUser = CreateUserObject(name); - dbContext.Users.Add(newUser); - dbContext.SaveChanges(); - - return newUser; - } - - public void DeleteUser(Data.Entities.User user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - using var dbContext = _dbProvider.CreateContext(); - - if (!dbContext.Users.Contains(user)) - { - throw new ArgumentException(string.Format( - CultureInfo.InvariantCulture, - "The user cannot be deleted because there is no user with the Name {0} and Id {1}.", - user.Username, - user.Id)); - } - - if (dbContext.Users.Count() == 1) - { - throw new InvalidOperationException(string.Format( - CultureInfo.InvariantCulture, - "The user '{0}' cannot be deleted because there must be at least one user in the system.", - user.Username)); - } - - if (user.HasPermission(PermissionKind.IsAdministrator) - && Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) - { - throw new ArgumentException( - string.Format( - CultureInfo.InvariantCulture, - "The user '{0}' cannot be deleted because there must be at least one admin user in the system.", - user.Username), - nameof(user)); - } - - dbContext.Users.Remove(user); - dbContext.SaveChanges(); - } - - public Task ResetPassword(Data.Entities.User user) - { - return ChangePassword(user, string.Empty); - } - - public void ResetEasyPassword(Data.Entities.User user) - { - ChangeEasyPassword(user, string.Empty, null); - } - - public async Task ChangePassword(Data.Entities.User user, string newPassword) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); - await UpdateUserAsync(user).ConfigureAwait(false); - - OnUserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); - } - - public void ChangeEasyPassword(Data.Entities.User user, string newPassword, string newPasswordSha1) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordSha1); - - UpdateUser(user); - - OnUserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); - } - - public UserDto GetUserDto(Data.Entities.User user, string remoteEndPoint = null) - { - return new UserDto - { - Id = user.Id, - HasPassword = user.Password == null, - EnableAutoLogin = user.EnableAutoLogin, - LastLoginDate = user.LastLoginDate, - LastActivityDate = user.LastActivityDate, - Configuration = new UserConfiguration - { - SubtitleMode = user.SubtitleMode, - HidePlayedInLatest = user.HidePlayedInLatest, - EnableLocalPassword = user.EnableLocalPassword, - PlayDefaultAudioTrack = user.PlayDefaultAudioTrack, - DisplayCollectionsView = user.DisplayCollectionsView, - DisplayMissingEpisodes = user.DisplayMissingEpisodes, - AudioLanguagePreference = user.AudioLanguagePreference, - RememberAudioSelections = user.RememberAudioSelections, - EnableNextEpisodeAutoPlay = user.EnableNextEpisodeAutoPlay, - RememberSubtitleSelections = user.RememberSubtitleSelections, - SubtitleLanguagePreference = user.SubtitleLanguagePreference, - OrderedViews = user.GetPreference(PreferenceKind.OrderedViews), - GroupedFolders = user.GetPreference(PreferenceKind.GroupedFolders), - MyMediaExcludes = user.GetPreference(PreferenceKind.MyMediaExcludes), - LatestItemsExcludes = user.GetPreference(PreferenceKind.LatestItemExcludes) - }, - Policy = new UserPolicy - { - MaxParentalRating = user.MaxParentalAgeRating, - EnableUserPreferenceAccess = user.EnableUserPreferenceAccess, - RemoteClientBitrateLimit = user.RemoteClientBitrateLimit.GetValueOrDefault(), - AuthenticatioIsnProviderId = user.AuthenticationProviderId, - PasswordResetProviderId = user.PasswordResetProviderId, - InvalidLoginAttemptCount = user.InvalidLoginAttemptCount, - LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout.GetValueOrDefault(), - IsAdministrator = user.HasPermission(PermissionKind.IsAdministrator), - IsHidden = user.HasPermission(PermissionKind.IsHidden), - IsDisabled = user.HasPermission(PermissionKind.IsDisabled), - EnableSharedDeviceControl = user.HasPermission(PermissionKind.EnableSharedDeviceControl), - EnableRemoteAccess = user.HasPermission(PermissionKind.EnableRemoteAccess), - EnableLiveTvManagement = user.HasPermission(PermissionKind.EnableLiveTvManagement), - EnableLiveTvAccess = user.HasPermission(PermissionKind.EnableLiveTvAccess), - EnableMediaPlayback = user.HasPermission(PermissionKind.EnableMediaPlayback), - EnableAudioPlaybackTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding), - EnableVideoPlaybackTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding), - EnableContentDeletion = user.HasPermission(PermissionKind.EnableContentDeletion), - EnableContentDownloading = user.HasPermission(PermissionKind.EnableContentDownloading), - EnableSyncTranscoding = user.HasPermission(PermissionKind.EnableSyncTranscoding), - EnableMediaConversion = user.HasPermission(PermissionKind.EnableMediaConversion), - EnableAllChannels = user.HasPermission(PermissionKind.EnableAllChannels), - EnableAllDevices = user.HasPermission(PermissionKind.EnableAllDevices), - EnableAllFolders = user.HasPermission(PermissionKind.EnableAllFolders), - EnableRemoteControlOfOtherUsers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers), - EnablePlaybackRemuxing = user.HasPermission(PermissionKind.EnablePlaybackRemuxing), - ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding), - EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing), - AccessSchedules = user.AccessSchedules.ToArray(), - BlockedTags = user.GetPreference(PreferenceKind.BlockedTags), - EnabledChannels = user.GetPreference(PreferenceKind.EnabledChannels), - EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices), - EnabledFolders = user.GetPreference(PreferenceKind.EnabledFolders), - EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders) - } - }; - } - - public PublicUserDto GetPublicUserDto(Data.Entities.User user, string remoteEndPoint = null) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user); - bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetAuthenticationProvider(user).GetEasyPasswordHash(user)); - - bool hasPassword = user.EnableLocalPassword && - !string.IsNullOrEmpty(remoteEndPoint) && - _networkManager.IsInLocalNetwork(remoteEndPoint) ? hasConfiguredEasyPassword : hasConfiguredPassword; - - return new PublicUserDto - { - Name = user.Username, - HasPassword = hasPassword, - HasConfiguredPassword = hasConfiguredPassword - }; - } - - public async Task AuthenticateUser( - string username, - string password, - string passwordSha1, - string remoteEndPoint, - bool isUserSession) - { - if (string.IsNullOrWhiteSpace(username)) - { - _logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndPoint); - throw new ArgumentNullException(nameof(username)); - } - - var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); - bool success; - IAuthenticationProvider authenticationProvider; - - if (user != null) - { - var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint) - .ConfigureAwait(false); - authenticationProvider = authResult.authenticationProvider; - success = authResult.success; - } - else - { - var authResult = await AuthenticateLocalUser(username, password, null, remoteEndPoint) - .ConfigureAwait(false); - authenticationProvider = authResult.authenticationProvider; - string updatedUsername = authResult.username; - success = authResult.success; - - if (success - && authenticationProvider != null - && !(authenticationProvider is DefaultAuthenticationProvider)) - { - // Trust the username returned by the authentication provider - username = updatedUsername; - - // Search the database for the user again - // the authentication provider might have created it - user = Users - .FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); - - if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy) - { - UpdatePolicy(user.Id, hasNewUserPolicy.GetNewUserPolicy()); - - await UpdateUserAsync(user).ConfigureAwait(false); - } - } - } - - if (success && user != null && authenticationProvider != null) - { - var providerId = authenticationProvider.GetType().FullName; - - if (!string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase)) - { - user.AuthenticationProviderId = providerId; - await UpdateUserAsync(user).ConfigureAwait(false); - } - } - - if (user == null) - { - _logger.LogInformation( - "Authentication request for {UserName} has been denied (IP: {IP}).", - username, - remoteEndPoint); - throw new AuthenticationException("Invalid username or password entered."); - } - - if (user.HasPermission(PermissionKind.IsDisabled)) - { - _logger.LogInformation( - "Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).", - username, - remoteEndPoint); - throw new SecurityException( - $"The {user.Username} account is currently disabled. Please consult with your administrator."); - } - - if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && - !_networkManager.IsInLocalNetwork(remoteEndPoint)) - { - _logger.LogInformation( - "Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).", - username, - remoteEndPoint); - throw new SecurityException("Forbidden."); - } - - if (!user.IsParentalScheduleAllowed()) - { - _logger.LogInformation( - "Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).", - username, - remoteEndPoint); - throw new SecurityException("User is not allowed access at this time."); - } - - // Update LastActivityDate and LastLoginDate, then save - if (success) - { - if (isUserSession) - { - user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; - UpdateUser(user); - } - - ResetInvalidLoginAttemptCount(user); - _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username); - } - else - { - IncrementInvalidLoginAttemptCount(user); - _logger.LogInformation( - "Authentication request for {UserName} has been denied (IP: {IP}).", - user.Username, - remoteEndPoint); - } - - return success ? user : null; - } - - public async Task StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) - { - var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername); - - var action = ForgotPasswordAction.InNetworkRequired; - - if (user != null && isInNetwork) - { - var passwordResetProvider = GetPasswordResetProvider(user); - return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false); - } - - return new ForgotPasswordResult - { - Action = action, - PinFile = string.Empty - }; - } - - public async Task RedeemPasswordResetPin(string pin) - { - foreach (var provider in _passwordResetProviders) - { - var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false); - - if (result.Success) - { - return result; - } - } - - return new PinRedeemResult - { - Success = false, - UsersReset = Array.Empty() - }; - } - - public void AddParts(IEnumerable authenticationProviders, IEnumerable passwordResetProviders) - { - _authenticationProviders = authenticationProviders.ToArray(); - - _defaultAuthenticationProvider = _authenticationProviders.OfType().First(); - - _invalidAuthProvider = _authenticationProviders.OfType().First(); - - _passwordResetProviders = passwordResetProviders.ToArray(); - - _defaultPasswordResetProvider = passwordResetProviders.OfType().First(); - } - - public NameIdPair[] GetAuthenticationProviders() - { - return _authenticationProviders - .Where(provider => provider.IsEnabled) - .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1) - .ThenBy(i => i.Name) - .Select(i => new NameIdPair - { - Name = i.Name, - Id = i.GetType().FullName - }) - .ToArray(); - } - - public NameIdPair[] GetPasswordResetProviders() - { - return _passwordResetProviders - .Where(provider => provider.IsEnabled) - .OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1) - .ThenBy(i => i.Name) - .Select(i => new NameIdPair - { - Name = i.Name, - Id = i.GetType().FullName - }) - .ToArray(); - } - - public void UpdateConfiguration(Guid userId, UserConfiguration config) - { - var user = GetUserById(userId); - user.SubtitleMode = config.SubtitleMode; - user.HidePlayedInLatest = config.HidePlayedInLatest; - user.EnableLocalPassword = config.EnableLocalPassword; - user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack; - user.DisplayCollectionsView = config.DisplayCollectionsView; - user.DisplayMissingEpisodes = config.DisplayMissingEpisodes; - user.AudioLanguagePreference = config.AudioLanguagePreference; - user.RememberAudioSelections = config.RememberAudioSelections; - user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay; - user.RememberSubtitleSelections = config.RememberSubtitleSelections; - user.SubtitleLanguagePreference = config.SubtitleLanguagePreference; - - user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews); - user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders); - user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes); - user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes); - - UpdateUser(user); - } - - public void UpdatePolicy(Guid userId, UserPolicy policy) - { - var user = GetUserById(userId); - - user.MaxParentalAgeRating = policy.MaxParentalRating; - user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess; - user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit; - user.AuthenticationProviderId = policy.AuthenticatioIsnProviderId; - user.PasswordResetProviderId = policy.PasswordResetProviderId; - user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount; - user.LoginAttemptsBeforeLockout = policy.LoginAttemptsBeforeLockout == -1 - ? null - : new int?(policy.LoginAttemptsBeforeLockout); - user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator); - user.SetPermission(PermissionKind.IsHidden, policy.IsHidden); - user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled); - user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl); - user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess); - user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement); - user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess); - user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback); - user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding); - user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding); - user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion); - user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading); - user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding); - user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion); - user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels); - user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices); - user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders); - user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers); - user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing); - user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding); - user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing); - - user.AccessSchedules.Clear(); - foreach (var policyAccessSchedule in policy.AccessSchedules) - { - user.AccessSchedules.Add(policyAccessSchedule); - } - - user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); - user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels); - user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices); - user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders); - user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders); - } - - private Data.Entities.User CreateUserObject(string name) - { - return new Data.Entities.User( - username: name, - mustUpdatePassword: false, - authenticationProviderId: _defaultAuthenticationProvider.GetType().FullName, - invalidLoginAttemptCount: -1, - subtitleMode: SubtitlePlaybackMode.Default, - playDefaultAudioTrack: true); - } - - private IAuthenticationProvider GetAuthenticationProvider(Data.Entities.User user) - { - return GetAuthenticationProviders(user)[0]; - } - - private IPasswordResetProvider GetPasswordResetProvider(Data.Entities.User user) - { - return GetPasswordResetProviders(user)[0]; - } - - private IList GetAuthenticationProviders(Data.Entities.User user) - { - var authenticationProviderId = user?.AuthenticationProviderId; - - var providers = _authenticationProviders.Where(i => i.IsEnabled).ToList(); - - if (!string.IsNullOrEmpty(authenticationProviderId)) - { - providers = providers.Where(i => string.Equals(authenticationProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)).ToList(); - } - - if (providers.Count == 0) - { - // Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found - _logger.LogWarning( - "User {UserName} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. Assigning user to InvalidAuthProvider until this is corrected", - user?.Username, - user?.AuthenticationProviderId); - providers = new List - { - _invalidAuthProvider - }; - } - - return providers; - } - - private IList GetPasswordResetProviders(Data.Entities.User user) - { - var passwordResetProviderId = user?.PasswordResetProviderId; - var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray(); - - if (!string.IsNullOrEmpty(passwordResetProviderId)) - { - providers = providers.Where(i => - string.Equals(passwordResetProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)) - .ToArray(); - } - - if (providers.Length == 0) - { - providers = new IPasswordResetProvider[] - { - _defaultPasswordResetProvider - }; - } - - return providers; - } - - private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)> - AuthenticateLocalUser( - string username, - string password, - Jellyfin.Data.Entities.User user, - string remoteEndPoint) - { - bool success = false; - IAuthenticationProvider authenticationProvider = null; - - foreach (var provider in GetAuthenticationProviders(user)) - { - var providerAuthResult = - await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); - var updatedUsername = providerAuthResult.username; - success = providerAuthResult.success; - - if (success) - { - authenticationProvider = provider; - username = updatedUsername; - break; - } - } - - if (!success - && _networkManager.IsInLocalNetwork(remoteEndPoint) - && user?.EnableLocalPassword == true - && !string.IsNullOrEmpty(user.EasyPassword)) - { - // Check easy password - var passwordHash = PasswordHash.Parse(user.EasyPassword); - var hash = _cryptoProvider.ComputeHash( - passwordHash.Id, - Encoding.UTF8.GetBytes(password), - passwordHash.Salt.ToArray()); - success = passwordHash.Hash.SequenceEqual(hash); - } - - return (authenticationProvider, username, success); - } - - private async Task<(string username, bool success)> AuthenticateWithProvider( - IAuthenticationProvider provider, - string username, - string password, - Data.Entities.User resolvedUser) - { - try - { - var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser - ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false) - : await provider.Authenticate(username, password).ConfigureAwait(false); - - if (authenticationResult.Username != username) - { - _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username); - username = authenticationResult.Username; - } - - return (username, true); - } - catch (AuthenticationException ex) - { - _logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name); - - return (username, false); - } - } - - private void IncrementInvalidLoginAttemptCount(Data.Entities.User user) - { - int invalidLogins = user.InvalidLoginAttemptCount; - int? maxInvalidLogins = user.LoginAttemptsBeforeLockout; - if (maxInvalidLogins.HasValue - && invalidLogins >= maxInvalidLogins) - { - user.SetPermission(PermissionKind.IsDisabled, true); - OnUserLockedOut?.Invoke(this, new GenericEventArgs(user)); - _logger.LogWarning( - "Disabling user {UserName} due to {Attempts} unsuccessful login attempts.", - user.Username, - invalidLogins); - } - - UpdateUser(user); - } - - private void ResetInvalidLoginAttemptCount(Data.Entities.User user) - { - user.InvalidLoginAttemptCount = 0; - } - } -} diff --git a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs new file mode 100644 index 000000000..38494727c --- /dev/null +++ b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs @@ -0,0 +1,185 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Common; +using MediaBrowser.Common.Cryptography; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Model.Cryptography; + +namespace Jellyfin.Server.Implementations.Users +{ + /// + /// The default authentication provider. + /// + public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser + { + private readonly ICryptoProvider _cryptographyProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The cryptography provider. + public DefaultAuthenticationProvider(ICryptoProvider cryptographyProvider) + { + _cryptographyProvider = cryptographyProvider; + } + + /// + public string Name => "Default"; + + /// + public bool IsEnabled => true; + + /// + // This is dumb and an artifact of the backwards way auth providers were designed. + // This version of authenticate was never meant to be called, but needs to be here for interface compat + // Only the providers that don't provide local user support use this + public Task Authenticate(string username, string password) + { + throw new NotImplementedException(); + } + + /// + // This is the version that we need to use for local users. Because reasons. + public Task Authenticate(string username, string password, Data.Entities.User resolvedUser) + { + if (resolvedUser == null) + { + throw new AuthenticationException("Specified user does not exist."); + } + + bool success = false; + + // As long as jellyfin supports passwordless users, we need this little block here to accommodate + if (!HasPassword(resolvedUser)) + { + return Task.FromResult(new ProviderAuthenticationResult + { + Username = username + }); + } + + byte[] passwordBytes = Encoding.UTF8.GetBytes(password); + + PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password); + if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id) + || _cryptographyProvider.DefaultHashMethod == readyHash.Id) + { + byte[] calculatedHash = _cryptographyProvider.ComputeHash( + readyHash.Id, + passwordBytes, + readyHash.Salt.ToArray()); + + if (readyHash.Hash.SequenceEqual(calculatedHash)) + { + success = true; + } + } + else + { + throw new AuthenticationException($"Requested crypto method not available in provider: {readyHash.Id}"); + } + + if (!success) + { + throw new AuthenticationException("Invalid username or password"); + } + + return Task.FromResult(new ProviderAuthenticationResult + { + Username = username + }); + } + + /// + public bool HasPassword(Data.Entities.User user) + => !string.IsNullOrEmpty(user.Password); + + /// + public Task ChangePassword(Data.Entities.User user, string newPassword) + { + if (string.IsNullOrEmpty(newPassword)) + { + user.Password = null; + return Task.CompletedTask; + } + + PasswordHash newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword); + user.Password = newPasswordHash.ToString(); + + return Task.CompletedTask; + } + + /// + public void ChangeEasyPassword(Data.Entities.User user, string newPassword, string newPasswordHash) + { + if (newPassword != null) + { + newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword).ToString(); + } + + if (string.IsNullOrWhiteSpace(newPasswordHash)) + { + throw new ArgumentNullException(nameof(newPasswordHash)); + } + + user.EasyPassword = newPasswordHash; + } + + /// + public string GetEasyPasswordHash(Data.Entities.User user) + { + return string.IsNullOrEmpty(user.EasyPassword) + ? null + : Hex.Encode(PasswordHash.Parse(user.EasyPassword).Hash); + } + + /// + /// Hashes the provided string. + /// + /// The user. + /// The string to hash. + /// The hashed string. + public string GetHashedString(Data.Entities.User user, string str) + { + if (string.IsNullOrEmpty(user.Password)) + { + return _cryptographyProvider.CreatePasswordHash(str).ToString(); + } + + // TODO: make use of iterations parameter? + PasswordHash passwordHash = PasswordHash.Parse(user.Password); + var salt = passwordHash.Salt.ToArray(); + return new PasswordHash( + passwordHash.Id, + _cryptographyProvider.ComputeHash( + passwordHash.Id, + Encoding.UTF8.GetBytes(str), + salt), + salt, + passwordHash.Parameters.ToDictionary(x => x.Key, y => y.Value)).ToString(); + } + + /// + /// Hashes the provided string. + /// + /// The user. + /// The string to hash. + /// The hashed string. + public ReadOnlySpan GetHashed(Data.Entities.User user, string str) + { + if (string.IsNullOrEmpty(user.Password)) + { + return _cryptographyProvider.CreatePasswordHash(str).Hash; + } + + // TODO: make use of iterations parameter? + PasswordHash passwordHash = PasswordHash.Parse(user.Password); + return _cryptographyProvider.ComputeHash( + passwordHash.Id, + Encoding.UTF8.GetBytes(str), + passwordHash.Salt.ToArray()); + } + } +} diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs new file mode 100644 index 000000000..60b48ec76 --- /dev/null +++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Users; + +namespace Jellyfin.Server.Implementations.Users +{ + /// + /// The default password reset provider. + /// + public class DefaultPasswordResetProvider : IPasswordResetProvider + { + private const string BaseResetFileName = "passwordreset"; + + private readonly IJsonSerializer _jsonSerializer; + private readonly IUserManager _userManager; + + private readonly string _passwordResetFileBase; + private readonly string _passwordResetFileBaseDir; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration manager. + /// The JSON serializer. + /// The user manager. + public DefaultPasswordResetProvider( + IServerConfigurationManager configurationManager, + IJsonSerializer jsonSerializer, + IUserManager userManager) + { + _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; + _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName); + _jsonSerializer = jsonSerializer; + _userManager = userManager; + } + + /// + public string Name => "Default Password Reset Provider"; + + /// + public bool IsEnabled => true; + + /// + public async Task RedeemPasswordResetPin(string pin) + { + SerializablePasswordReset spr; + List usersReset = new List(); + foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*")) + { + await using (var str = File.OpenRead(resetFile)) + { + spr = await _jsonSerializer.DeserializeFromStreamAsync(str).ConfigureAwait(false); + } + + if (spr.ExpirationDate < DateTime.Now) + { + File.Delete(resetFile); + } + else if (string.Equals( + spr.Pin.Replace("-", string.Empty, StringComparison.Ordinal), + pin.Replace("-", string.Empty, StringComparison.Ordinal), + StringComparison.InvariantCultureIgnoreCase)) + { + var resetUser = _userManager.GetUserByName(spr.UserName); + if (resetUser == null) + { + throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found"); + } + + await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); + usersReset.Add(resetUser.Username); + File.Delete(resetFile); + } + } + + if (usersReset.Count < 1) + { + throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}"); + } + + return new PinRedeemResult + { + Success = true, + UsersReset = usersReset.ToArray() + }; + } + + /// + public async Task StartForgotPasswordProcess(User user, bool isInNetwork) + { + string pin; + using (var cryptoRandom = RandomNumberGenerator.Create()) + { + byte[] bytes = new byte[4]; + cryptoRandom.GetBytes(bytes); + pin = BitConverter.ToString(bytes); + } + + DateTime expireTime = DateTime.Now.AddMinutes(30); + + user.EasyPassword = pin; + await _userManager.UpdateUserAsync(user).ConfigureAwait(false); + + return new ForgotPasswordResult + { + Action = ForgotPasswordAction.PinCode, + PinExpirationDate = expireTime, + }; + } + + private class SerializablePasswordReset : PasswordPinCreationResult + { + public string Pin { get; set; } + + public string UserName { get; set; } + } + } +} diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs new file mode 100644 index 000000000..d94a27b9d --- /dev/null +++ b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs @@ -0,0 +1,66 @@ +#pragma warning disable CS1591 + +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Events; + +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) + { + _userManager = userManager; + _authRepo = authRepo; + _deviceManager = deviceManager; + _sessionManager = sessionManager; + } + + public Task RunAsync() + { + _userManager.OnUserUpdated += OnUserUpdated; + + return Task.CompletedTask; + } + + public void Dispose() + { + } + + private void OnUserUpdated(object sender, GenericEventArgs e) + { + var user = e.Argument; + if (!user.HasPermission(PermissionKind.EnableAllDevices)) + { + UpdateDeviceAccess(user); + } + } + + private void UpdateDeviceAccess(User user) + { + var existing = _authRepo.Get(new AuthenticationInfoQuery + { + UserId = user.Id + }).Items; + + foreach (var authInfo in existing) + { + if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId)) + { + _sessionManager.Logout(authInfo); + } + } + } + } +} diff --git a/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs new file mode 100644 index 000000000..e430808bf --- /dev/null +++ b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using MediaBrowser.Controller.Authentication; + +namespace Jellyfin.Server.Implementations.Users +{ + /// + /// An invalid authentication provider. + /// + public class InvalidAuthProvider : IAuthenticationProvider + { + /// + public string Name => "InvalidOrMissingAuthenticationProvider"; + + /// + public bool IsEnabled => true; + + /// + public Task Authenticate(string username, string password) + { + throw new AuthenticationException("User Account cannot login with this provider. The Normal provider for this user cannot be found"); + } + + /// + public bool HasPassword(Data.Entities.User user) + { + return true; + } + + /// + public Task ChangePassword(Data.Entities.User user, string newPassword) + { + return Task.CompletedTask; + } + + /// + public void ChangeEasyPassword(Data.Entities.User user, string newPassword, string newPasswordHash) + { + // Nothing here + } + + /// + public string GetEasyPasswordHash(Data.Entities.User user) + { + return string.Empty; + } + } +} diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs new file mode 100644 index 000000000..ddc05055b --- /dev/null +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -0,0 +1,763 @@ +#pragma warning disable CA1307 +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using MediaBrowser.Common.Cryptography; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.Users; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Implementations.Users +{ + public class UserManager : IUserManager + { + private readonly JellyfinDbProvider _dbProvider; + private readonly ICryptoProvider _cryptoProvider; + private readonly INetworkManager _networkManager; + private readonly ILogger _logger; + + private IAuthenticationProvider[] _authenticationProviders; + private DefaultAuthenticationProvider _defaultAuthenticationProvider; + private InvalidAuthProvider _invalidAuthProvider; + private IPasswordResetProvider[] _passwordResetProviders; + private DefaultPasswordResetProvider _defaultPasswordResetProvider; + + public UserManager( + JellyfinDbProvider dbProvider, + ICryptoProvider cryptoProvider, + INetworkManager networkManager, + ILogger logger) + { + _dbProvider = dbProvider; + _cryptoProvider = cryptoProvider; + _networkManager = networkManager; + _logger = logger; + } + + public event EventHandler> OnUserPasswordChanged; + + /// + public event EventHandler> OnUserUpdated; + + /// + public event EventHandler> OnUserCreated; + + /// + public event EventHandler> OnUserDeleted; + + public event EventHandler> OnUserLockedOut; + + public IEnumerable Users + { + get + { + var dbContext = _dbProvider.CreateContext(); + return dbContext.Users; + } + } + + public IEnumerable UsersIds + { + get + { + var dbContext = _dbProvider.CreateContext(); + return dbContext.Users.Select(u => u.Id); + } + } + + public User GetUserById(Guid id) + { + if (id == Guid.Empty) + { + throw new ArgumentException("Guid can't be empty", nameof(id)); + } + + var dbContext = _dbProvider.CreateContext(); + + return dbContext.Users.Find(id); + } + + public User GetUserByName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Invalid username", nameof(name)); + } + + var dbContext = _dbProvider.CreateContext(); + + // This can't use an overload with StringComparer because that would cause the query to + // have to be evaluated client-side. + return dbContext.Users.FirstOrDefault(u => string.Equals(u.Username, name)); + } + + public async Task RenameUser(User user, string newName) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (string.IsNullOrWhiteSpace(newName)) + { + throw new ArgumentException("Invalid username", nameof(newName)); + } + + if (user.Username.Equals(newName, StringComparison.Ordinal)) + { + throw new ArgumentException("The new and old names must be different."); + } + + if (Users.Any( + u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException(string.Format( + CultureInfo.InvariantCulture, + "A user with the name '{0}' already exists.", + newName)); + } + + user.Username = newName; + await UpdateUserAsync(user).ConfigureAwait(false); + + OnUserUpdated?.Invoke(this, new GenericEventArgs(user)); + } + + public void UpdateUser(User user) + { + var dbContext = _dbProvider.CreateContext(); + dbContext.Users.Update(user); + dbContext.SaveChanges(); + } + + public async Task UpdateUserAsync(User user) + { + var dbContext = _dbProvider.CreateContext(); + dbContext.Users.Update(user); + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + + public User CreateUser(string name) + { + if (!IsValidUsername(name)) + { + throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)"); + } + + var dbContext = _dbProvider.CreateContext(); + + var newUser = new User(name, _defaultAuthenticationProvider.GetType().FullName); + dbContext.Users.Add(newUser); + dbContext.SaveChanges(); + + OnUserCreated?.Invoke(this, new GenericEventArgs(newUser)); + + return newUser; + } + + public void DeleteUser(User user) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + var dbContext = _dbProvider.CreateContext(); + + if (!dbContext.Users.Contains(user)) + { + throw new ArgumentException(string.Format( + CultureInfo.InvariantCulture, + "The user cannot be deleted because there is no user with the Name {0} and Id {1}.", + user.Username, + user.Id)); + } + + if (dbContext.Users.Count() == 1) + { + throw new InvalidOperationException(string.Format( + CultureInfo.InvariantCulture, + "The user '{0}' cannot be deleted because there must be at least one user in the system.", + user.Username)); + } + + if (user.HasPermission(PermissionKind.IsAdministrator) + && Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) + { + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + "The user '{0}' cannot be deleted because there must be at least one admin user in the system.", + user.Username), + nameof(user)); + } + + dbContext.Users.Remove(user); + dbContext.SaveChanges(); + OnUserDeleted?.Invoke(this, new GenericEventArgs(user)); + } + + public Task ResetPassword(User user) + { + return ChangePassword(user, string.Empty); + } + + public void ResetEasyPassword(User user) + { + ChangeEasyPassword(user, string.Empty, null); + } + + public async Task ChangePassword(User user, string newPassword) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); + await UpdateUserAsync(user).ConfigureAwait(false); + + OnUserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); + } + + public void ChangeEasyPassword(User user, string newPassword, string newPasswordSha1) + { + GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordSha1); + UpdateUser(user); + + OnUserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); + } + + public UserDto GetUserDto(User user, string remoteEndPoint = null) + { + return new UserDto + { + Id = user.Id, + HasPassword = user.Password == null, + EnableAutoLogin = user.EnableAutoLogin, + LastLoginDate = user.LastLoginDate, + LastActivityDate = user.LastActivityDate, + Configuration = new UserConfiguration + { + SubtitleMode = user.SubtitleMode, + HidePlayedInLatest = user.HidePlayedInLatest, + EnableLocalPassword = user.EnableLocalPassword, + PlayDefaultAudioTrack = user.PlayDefaultAudioTrack, + DisplayCollectionsView = user.DisplayCollectionsView, + DisplayMissingEpisodes = user.DisplayMissingEpisodes, + AudioLanguagePreference = user.AudioLanguagePreference, + RememberAudioSelections = user.RememberAudioSelections, + EnableNextEpisodeAutoPlay = user.EnableNextEpisodeAutoPlay, + RememberSubtitleSelections = user.RememberSubtitleSelections, + SubtitleLanguagePreference = user.SubtitleLanguagePreference, + OrderedViews = user.GetPreference(PreferenceKind.OrderedViews), + GroupedFolders = user.GetPreference(PreferenceKind.GroupedFolders), + MyMediaExcludes = user.GetPreference(PreferenceKind.MyMediaExcludes), + LatestItemsExcludes = user.GetPreference(PreferenceKind.LatestItemExcludes) + }, + Policy = new UserPolicy + { + MaxParentalRating = user.MaxParentalAgeRating, + EnableUserPreferenceAccess = user.EnableUserPreferenceAccess, + RemoteClientBitrateLimit = user.RemoteClientBitrateLimit.GetValueOrDefault(), + AuthenticationProviderId = user.AuthenticationProviderId, + PasswordResetProviderId = user.PasswordResetProviderId, + InvalidLoginAttemptCount = user.InvalidLoginAttemptCount, + LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout.GetValueOrDefault(), + IsAdministrator = user.HasPermission(PermissionKind.IsAdministrator), + IsHidden = user.HasPermission(PermissionKind.IsHidden), + IsDisabled = user.HasPermission(PermissionKind.IsDisabled), + EnableSharedDeviceControl = user.HasPermission(PermissionKind.EnableSharedDeviceControl), + EnableRemoteAccess = user.HasPermission(PermissionKind.EnableRemoteAccess), + EnableLiveTvManagement = user.HasPermission(PermissionKind.EnableLiveTvManagement), + EnableLiveTvAccess = user.HasPermission(PermissionKind.EnableLiveTvAccess), + EnableMediaPlayback = user.HasPermission(PermissionKind.EnableMediaPlayback), + EnableAudioPlaybackTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding), + EnableVideoPlaybackTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding), + EnableContentDeletion = user.HasPermission(PermissionKind.EnableContentDeletion), + EnableContentDownloading = user.HasPermission(PermissionKind.EnableContentDownloading), + EnableSyncTranscoding = user.HasPermission(PermissionKind.EnableSyncTranscoding), + EnableMediaConversion = user.HasPermission(PermissionKind.EnableMediaConversion), + EnableAllChannels = user.HasPermission(PermissionKind.EnableAllChannels), + EnableAllDevices = user.HasPermission(PermissionKind.EnableAllDevices), + EnableAllFolders = user.HasPermission(PermissionKind.EnableAllFolders), + EnableRemoteControlOfOtherUsers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers), + EnablePlaybackRemuxing = user.HasPermission(PermissionKind.EnablePlaybackRemuxing), + ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding), + EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing), + AccessSchedules = user.AccessSchedules.ToArray(), + BlockedTags = user.GetPreference(PreferenceKind.BlockedTags), + EnabledChannels = user.GetPreference(PreferenceKind.EnabledChannels), + EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices), + EnabledFolders = user.GetPreference(PreferenceKind.EnabledFolders), + EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders) + } + }; + } + + public PublicUserDto GetPublicUserDto(User user, string remoteEndPoint = null) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user); + bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetAuthenticationProvider(user).GetEasyPasswordHash(user)); + + bool hasPassword = user.EnableLocalPassword && + !string.IsNullOrEmpty(remoteEndPoint) && + _networkManager.IsInLocalNetwork(remoteEndPoint) ? hasConfiguredEasyPassword : hasConfiguredPassword; + + return new PublicUserDto + { + Name = user.Username, + HasPassword = hasPassword, + HasConfiguredPassword = hasConfiguredPassword + }; + } + + public async Task AuthenticateUser( + string username, + string password, + string passwordSha1, + string remoteEndPoint, + bool isUserSession) + { + if (string.IsNullOrWhiteSpace(username)) + { + _logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndPoint); + throw new ArgumentNullException(nameof(username)); + } + + var user = Users.ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); + bool success; + IAuthenticationProvider authenticationProvider; + + if (user != null) + { + var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint) + .ConfigureAwait(false); + authenticationProvider = authResult.authenticationProvider; + success = authResult.success; + } + else + { + var authResult = await AuthenticateLocalUser(username, password, null, remoteEndPoint) + .ConfigureAwait(false); + authenticationProvider = authResult.authenticationProvider; + string updatedUsername = authResult.username; + success = authResult.success; + + if (success + && authenticationProvider != null + && !(authenticationProvider is DefaultAuthenticationProvider)) + { + // Trust the username returned by the authentication provider + username = updatedUsername; + + // Search the database for the user again + // the authentication provider might have created it + user = Users + .ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); + + if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy) + { + UpdatePolicy(user.Id, hasNewUserPolicy.GetNewUserPolicy()); + + await UpdateUserAsync(user).ConfigureAwait(false); + } + } + } + + if (success && user != null && authenticationProvider != null) + { + var providerId = authenticationProvider.GetType().FullName; + + if (!string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase)) + { + user.AuthenticationProviderId = providerId; + await UpdateUserAsync(user).ConfigureAwait(false); + } + } + + if (user == null) + { + _logger.LogInformation( + "Authentication request for {UserName} has been denied (IP: {IP}).", + username, + remoteEndPoint); + throw new AuthenticationException("Invalid username or password entered."); + } + + if (user.HasPermission(PermissionKind.IsDisabled)) + { + _logger.LogInformation( + "Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).", + username, + remoteEndPoint); + throw new SecurityException( + $"The {user.Username} account is currently disabled. Please consult with your administrator."); + } + + if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && + !_networkManager.IsInLocalNetwork(remoteEndPoint)) + { + _logger.LogInformation( + "Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).", + username, + remoteEndPoint); + throw new SecurityException("Forbidden."); + } + + if (!user.IsParentalScheduleAllowed()) + { + _logger.LogInformation( + "Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).", + username, + remoteEndPoint); + throw new SecurityException("User is not allowed access at this time."); + } + + // Update LastActivityDate and LastLoginDate, then save + if (success) + { + if (isUserSession) + { + user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; + await UpdateUserAsync(user).ConfigureAwait(false); + } + + user.InvalidLoginAttemptCount = 0; + _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username); + } + else + { + IncrementInvalidLoginAttemptCount(user); + _logger.LogInformation( + "Authentication request for {UserName} has been denied (IP: {IP}).", + user.Username, + remoteEndPoint); + } + + return success ? user : null; + } + + public async Task StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) + { + var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername); + + var action = ForgotPasswordAction.InNetworkRequired; + + if (user != null && isInNetwork) + { + var passwordResetProvider = GetPasswordResetProvider(user); + return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false); + } + + return new ForgotPasswordResult + { + Action = action, + PinFile = string.Empty + }; + } + + public async Task RedeemPasswordResetPin(string pin) + { + foreach (var provider in _passwordResetProviders) + { + var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false); + + if (result.Success) + { + return result; + } + } + + return new PinRedeemResult + { + Success = false, + UsersReset = Array.Empty() + }; + } + + public void AddParts(IEnumerable authenticationProviders, IEnumerable passwordResetProviders) + { + _authenticationProviders = authenticationProviders.ToArray(); + _passwordResetProviders = passwordResetProviders.ToArray(); + + _invalidAuthProvider = _authenticationProviders.OfType().First(); + _defaultAuthenticationProvider = _authenticationProviders.OfType().First(); + _defaultPasswordResetProvider = _passwordResetProviders.OfType().First(); + } + + public NameIdPair[] GetAuthenticationProviders() + { + return _authenticationProviders + .Where(provider => provider.IsEnabled) + .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1) + .ThenBy(i => i.Name) + .Select(i => new NameIdPair + { + Name = i.Name, + Id = i.GetType().FullName + }) + .ToArray(); + } + + public NameIdPair[] GetPasswordResetProviders() + { + return _passwordResetProviders + .Where(provider => provider.IsEnabled) + .OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1) + .ThenBy(i => i.Name) + .Select(i => new NameIdPair + { + Name = i.Name, + Id = i.GetType().FullName + }) + .ToArray(); + } + + public void UpdateConfiguration(Guid userId, UserConfiguration config) + { + var user = GetUserById(userId); + user.SubtitleMode = config.SubtitleMode; + user.HidePlayedInLatest = config.HidePlayedInLatest; + user.EnableLocalPassword = config.EnableLocalPassword; + user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack; + user.DisplayCollectionsView = config.DisplayCollectionsView; + user.DisplayMissingEpisodes = config.DisplayMissingEpisodes; + user.AudioLanguagePreference = config.AudioLanguagePreference; + user.RememberAudioSelections = config.RememberAudioSelections; + user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay; + user.RememberSubtitleSelections = config.RememberSubtitleSelections; + user.SubtitleLanguagePreference = config.SubtitleLanguagePreference; + + user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews); + user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders); + user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes); + user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes); + + UpdateUser(user); + } + + public void UpdatePolicy(Guid userId, UserPolicy policy) + { + var user = GetUserById(userId); + + user.MaxParentalAgeRating = policy.MaxParentalRating; + user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess; + user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit; + user.AuthenticationProviderId = policy.AuthenticationProviderId; + user.PasswordResetProviderId = policy.PasswordResetProviderId; + user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount; + user.LoginAttemptsBeforeLockout = policy.LoginAttemptsBeforeLockout == -1 + ? null + : new int?(policy.LoginAttemptsBeforeLockout); + user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator); + user.SetPermission(PermissionKind.IsHidden, policy.IsHidden); + user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled); + user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl); + user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess); + user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement); + user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess); + user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback); + user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding); + user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding); + user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion); + user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading); + user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding); + user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion); + user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels); + user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices); + user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders); + user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers); + user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing); + user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding); + user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing); + + user.AccessSchedules.Clear(); + foreach (var policyAccessSchedule in policy.AccessSchedules) + { + user.AccessSchedules.Add(policyAccessSchedule); + } + + user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); + user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels); + user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices); + user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders); + user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders); + } + + private bool IsValidUsername(string name) + { + // This is some regex that matches only on unicode "word" characters, as well as -, _ and @ + // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness + // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.) + return Regex.IsMatch(name, @"^[\w\-'._@]*$"); + } + + private IAuthenticationProvider GetAuthenticationProvider(User user) + { + return GetAuthenticationProviders(user)[0]; + } + + private IPasswordResetProvider GetPasswordResetProvider(User user) + { + return GetPasswordResetProviders(user)[0]; + } + + private IList GetAuthenticationProviders(User user) + { + var authenticationProviderId = user?.AuthenticationProviderId; + + var providers = _authenticationProviders.Where(i => i.IsEnabled).ToList(); + + if (!string.IsNullOrEmpty(authenticationProviderId)) + { + providers = providers.Where(i => string.Equals(authenticationProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)).ToList(); + } + + if (providers.Count == 0) + { + // Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found + _logger.LogWarning( + "User {Username} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. Assigning user to InvalidAuthProvider until this is corrected", + user?.Username, + user?.AuthenticationProviderId); + providers = new List + { + _invalidAuthProvider + }; + } + + return providers; + } + + private IList GetPasswordResetProviders(User user) + { + var passwordResetProviderId = user?.PasswordResetProviderId; + var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray(); + + if (!string.IsNullOrEmpty(passwordResetProviderId)) + { + providers = providers.Where(i => + string.Equals(passwordResetProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + } + + if (providers.Length == 0) + { + providers = new IPasswordResetProvider[] + { + _defaultPasswordResetProvider + }; + } + + return providers; + } + + private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)> AuthenticateLocalUser( + string username, + string password, + User user, + string remoteEndPoint) + { + bool success = false; + IAuthenticationProvider authenticationProvider = null; + + foreach (var provider in GetAuthenticationProviders(user)) + { + var providerAuthResult = + await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); + var updatedUsername = providerAuthResult.username; + success = providerAuthResult.success; + + if (success) + { + authenticationProvider = provider; + username = updatedUsername; + break; + } + } + + if (!success + && _networkManager.IsInLocalNetwork(remoteEndPoint) + && user?.EnableLocalPassword == true + && !string.IsNullOrEmpty(user.EasyPassword)) + { + // Check easy password + var passwordHash = PasswordHash.Parse(user.EasyPassword); + var hash = _cryptoProvider.ComputeHash( + passwordHash.Id, + Encoding.UTF8.GetBytes(password), + passwordHash.Salt.ToArray()); + success = passwordHash.Hash.SequenceEqual(hash); + } + + return (authenticationProvider, username, success); + } + + private async Task<(string username, bool success)> AuthenticateWithProvider( + IAuthenticationProvider provider, + string username, + string password, + User resolvedUser) + { + try + { + var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser + ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false) + : await provider.Authenticate(username, password).ConfigureAwait(false); + + if (authenticationResult.Username != username) + { + _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username); + username = authenticationResult.Username; + } + + return (username, true); + } + catch (AuthenticationException ex) + { + _logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name); + + return (username, false); + } + } + + private void IncrementInvalidLoginAttemptCount(User user) + { + int invalidLogins = user.InvalidLoginAttemptCount; + int? maxInvalidLogins = user.LoginAttemptsBeforeLockout; + if (maxInvalidLogins.HasValue && invalidLogins >= maxInvalidLogins) + { + user.SetPermission(PermissionKind.IsDisabled, true); + OnUserLockedOut?.Invoke(this, new GenericEventArgs(user)); + _logger.LogWarning( + "Disabling user {Username} due to {Attempts} unsuccessful login attempts.", + user.Username, + invalidLogins); + } + + UpdateUser(user); + } + } +} diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 331a32c73..b4c5fd4ea 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -7,8 +7,10 @@ using Emby.Server.Implementations; using Jellyfin.Drawing.Skia; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; +using Jellyfin.Server.Implementations.Users; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Library; using MediaBrowser.Model.Activity; using MediaBrowser.Model.IO; using Microsoft.EntityFrameworkCore; @@ -69,6 +71,7 @@ namespace Jellyfin.Server serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); base.RegisterServices(serviceCollection); } @@ -80,6 +83,9 @@ namespace Jellyfin.Server protected override IEnumerable GetAssembliesWithPartsInternal() { yield return typeof(CoreAppHost).Assembly; + yield return typeof(DefaultAuthenticationProvider).Assembly; + yield return typeof(DefaultPasswordResetProvider).Assembly; + yield return typeof(InvalidAuthProvider).Assembly; } /// diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index ea16c5573..9eec6ed4e 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -41,7 +41,6 @@ - diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index b19d7f7fc..0271098f4 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -1,33 +1,45 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; +using System; using System.IO; using Emby.Server.Implementations.Data; using Emby.Server.Implementations.Serialization; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Server.Implementations; +using Jellyfin.Server.Implementations.Users; using MediaBrowser.Controller; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Users; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; using SQLitePCL.pretty; +using JsonSerializer = System.Text.Json.JsonSerializer; namespace Jellyfin.Server.Migrations.Routines { + /// + /// The migration routine for migrating the user database to EF Core. + /// public class MigrateUserDb : IMigrationRoutine { - private readonly ILogger _logger; + private const string DbFilename = "users.db"; + private readonly ILogger _logger; private readonly IServerApplicationPaths _paths; - private readonly JellyfinDbProvider _provider; - private readonly MyXmlSerializer _xmlSerializer; - public MigrateUserDb(ILogger logger, IServerApplicationPaths paths, JellyfinDbProvider provider, MyXmlSerializer xmlSerializer) + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The server application paths. + /// The database provider. + /// The xml serializer. + public MigrateUserDb( + ILogger logger, + IServerApplicationPaths paths, + JellyfinDbProvider provider, + MyXmlSerializer xmlSerializer) { _logger = logger; _paths = paths; @@ -35,18 +47,21 @@ namespace Jellyfin.Server.Migrations.Routines _xmlSerializer = xmlSerializer; } + /// public Guid Id => Guid.Parse("5C4B82A2-F053-4009-BD05-B6FCAD82F14C"); - public string Name => "MigrateUserDb"; + /// + public string Name => "MigrateUserDatabase"; + /// public void Perform() { var dataPath = _paths.DataPath; _logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin."); - using (var connection = SQLite3.Open(Path.Combine(dataPath, "users.db"), ConnectionFlags.ReadOnly, null)) + using (var connection = SQLite3.Open(Path.Combine(dataPath, DbFilename), ConnectionFlags.ReadOnly, null)) { - using var dbContext = _provider.CreateContext(); + var dbContext = _provider.CreateContext(); var queryResult = connection.Query("SELECT * FROM LocalUsersv2"); @@ -55,26 +70,30 @@ namespace Jellyfin.Server.Migrations.Routines foreach (var entry in queryResult) { - var json = JsonConvert.DeserializeObject>(entry[2].ToString()); - var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, json["Name"]); - - var config = (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), Path.Combine(userDataDir, "config.xml")); - var policy = (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), Path.Combine(userDataDir, "policy.xml")); - - var user = new User( - json["Name"], - false, - policy.AuthenticatioIsnProviderId, - policy.InvalidLoginAttemptCount, - config.SubtitleMode, - config.PlayDefaultAudioTrack) + UserMockup mockup = JsonSerializer.Deserialize(entry[2].ToBlob()); + var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, mockup.Name); + + var config = File.Exists(Path.Combine(userDataDir, "config.xml")) + ? (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), Path.Combine(userDataDir, "config.xml")) + : new UserConfiguration(); + var policy = File.Exists(Path.Combine(userDataDir, "policy.xml")) + ? (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), Path.Combine(userDataDir, "policy.xml")) + : new UserPolicy(); + policy.AuthenticationProviderId = policy.AuthenticationProviderId?.Replace( + "Emby.Server.Implementations.Library", + "Jellyfin.Server.Implementations.Users", + StringComparison.Ordinal) + ?? typeof(DefaultAuthenticationProvider).FullName; + + policy.PasswordResetProviderId ??= typeof(DefaultPasswordResetProvider).FullName; + + var user = new User(mockup.Name, policy.AuthenticationProviderId) { Id = entry[1].ReadGuidFromBlob(), InternalId = entry[0].ToInt64(), MaxParentalAgeRating = policy.MaxParentalRating, EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess, RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit, - AuthenticationProviderId = policy.AuthenticatioIsnProviderId, PasswordResetProviderId = policy.PasswordResetProviderId, InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount, LoginAttemptsBeforeLockout = policy.LoginAttemptsBeforeLockout == -1 ? null : new int?(policy.LoginAttemptsBeforeLockout), @@ -89,6 +108,10 @@ namespace Jellyfin.Server.Migrations.Routines EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay, RememberSubtitleSelections = config.RememberSubtitleSelections, SubtitleLanguagePreference = config.SubtitleLanguagePreference, + Password = mockup.Password, + EasyPassword = mockup.EasyPassword, + LastLoginDate = mockup.LastLoginDate ?? DateTime.MinValue, + LastActivityDate = mockup.LastActivityDate ?? DateTime.MinValue }; user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator); @@ -112,6 +135,7 @@ namespace Jellyfin.Server.Migrations.Routines user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing); user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding); user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing); + foreach (var policyAccessSchedule in policy.AccessSchedules) { user.AccessSchedules.Add(policyAccessSchedule); @@ -126,6 +150,8 @@ namespace Jellyfin.Server.Migrations.Routines user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders); user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes); user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes); + + dbContext.Users.Add(user); } dbContext.SaveChanges(); @@ -133,12 +159,32 @@ namespace Jellyfin.Server.Migrations.Routines try { - File.Move(Path.Combine(dataPath, "users.db"), Path.Combine(dataPath, "users.db" + ".old")); + File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old")); + + var journalPath = Path.Combine(dataPath, DbFilename + "-journal"); + if (File.Exists(journalPath)) + { + File.Move(journalPath, Path.Combine(dataPath, DbFilename + ".old-journal")); + } } catch (IOException e) { _logger.LogError(e, "Error renaming legacy user database to 'users.db.old'"); } } + +#nullable disable + internal class UserMockup + { + public string Password { get; set; } + + public string EasyPassword { get; set; } + + public DateTime? LastLoginDate { get; set; } + + public DateTime? LastActivityDate { get; set; } + + public string Name { get; set; } + } } } diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 8d94d3971..bc9724027 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -1,6 +1,8 @@ #pragma warning disable CS1591 using System; +using System.Text.Json.Serialization; +using System.Xml.Serialization; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; @@ -34,7 +36,7 @@ namespace MediaBrowser.Model.Users public string[] BlockedTags { get; set; } public bool EnableUserPreferenceAccess { get; set; } - public Jellyfin.Data.Entities.AccessSchedule[] AccessSchedules { get; set; } + public AccessSchedule[] AccessSchedules { get; set; } public UnratedItem[] BlockUnratedItems { get; set; } public bool EnableRemoteControlOfOtherUsers { get; set; } public bool EnableSharedDeviceControl { get; set; } @@ -78,7 +80,9 @@ namespace MediaBrowser.Model.Users public string[] BlockedChannels { get; set; } public int RemoteClientBitrateLimit { get; set; } - public string AuthenticatioIsnProviderId { get; set; } + + [XmlElement(ElementName = "AuthenticationProviderId")] + public string AuthenticationProviderId { get; set; } public string PasswordResetProviderId { get; set; } public UserPolicy() -- cgit v1.2.3