diff options
Diffstat (limited to 'Jellyfin.Server.Implementations')
11 files changed, 885 insertions, 112 deletions
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs index f899b4497..b5f18d983 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs @@ -2,9 +2,8 @@ using System.Globalization; using System.Threading.Tasks; using Jellyfin.Data.Entities; -using Jellyfin.Data.Events; using MediaBrowser.Controller.Events; -using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.Events.Authentication; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Globalization; using Microsoft.Extensions.Logging; @@ -14,7 +13,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security /// <summary> /// Creates an entry in the activity log when there is a failed login attempt. /// </summary> - public class AuthenticationFailedLogger : IEventConsumer<GenericEventArgs<AuthenticationRequest>> + public class AuthenticationFailedLogger : IEventConsumer<AuthenticationRequestEventArgs> { private readonly ILocalizationManager _localizationManager; private readonly IActivityManager _activityManager; @@ -31,13 +30,13 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security } /// <inheritdoc /> - public async Task OnEvent(GenericEventArgs<AuthenticationRequest> eventArgs) + public async Task OnEvent(AuthenticationRequestEventArgs eventArgs) { await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("FailedLoginAttemptWithUserName"), - eventArgs.Argument.Username), + eventArgs.Username), "AuthenticationFailed", Guid.Empty) { @@ -45,7 +44,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security ShortOverview = string.Format( CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("LabelIpAddressValue"), - eventArgs.Argument.RemoteEndPoint), + eventArgs.RemoteEndPoint), }).ConfigureAwait(false); } } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs index 8b0bd84c6..2ee5b4e88 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs @@ -2,8 +2,8 @@ using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Events; -using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Events.Authentication; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Globalization; @@ -12,7 +12,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security /// <summary> /// Creates an entry in the activity log when there is a successful login attempt. /// </summary> - public class AuthenticationSucceededLogger : IEventConsumer<GenericEventArgs<AuthenticationResult>> + public class AuthenticationSucceededLogger : IEventConsumer<AuthenticationResultEventArgs> { private readonly ILocalizationManager _localizationManager; private readonly IActivityManager _activityManager; @@ -29,20 +29,20 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security } /// <inheritdoc /> - public async Task OnEvent(GenericEventArgs<AuthenticationResult> eventArgs) + public async Task OnEvent(AuthenticationResultEventArgs eventArgs) { await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("AuthenticationSucceededWithUserName"), - eventArgs.Argument.User.Name), + eventArgs.User.Name), "AuthenticationSucceeded", - eventArgs.Argument.User.Id) + eventArgs.User.Id) { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("LabelIpAddressValue"), - eventArgs.Argument.SessionInfo.RemoteEndPoint), + eventArgs.SessionInfo?.RemoteEndPoint), }).ConfigureAwait(false); } } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs index aeb62e814..27726a57a 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs @@ -58,15 +58,18 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session var user = eventArgs.Users[0]; await _activityManager.CreateAsync(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("UserStartedPlayingItemWithValues"), - user.Username, - GetItemName(eventArgs.MediaInfo), - eventArgs.DeviceName), - GetPlaybackNotificationType(eventArgs.MediaInfo.MediaType), - user.Id)) - .ConfigureAwait(false); + string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("UserStartedPlayingItemWithValues"), + user.Username, + GetItemName(eventArgs.MediaInfo), + eventArgs.DeviceName), + GetPlaybackNotificationType(eventArgs.MediaInfo.MediaType), + user.Id) + { + ItemId = eventArgs.Item?.Id.ToString("N", CultureInfo.InvariantCulture), + }) + .ConfigureAwait(false); } private static string GetItemName(BaseItemDto item) diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs index dd7290fb8..6b16477aa 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs @@ -73,7 +73,10 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session GetItemName(item), eventArgs.DeviceName), notificationType, - user.Id)) + user.Id) + { + ItemId = eventArgs.Item?.Id.ToString("N", CultureInfo.InvariantCulture), + }) .ConfigureAwait(false); } diff --git a/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs index 5d558189b..9a473de52 100644 --- a/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs @@ -8,12 +8,11 @@ using Jellyfin.Server.Implementations.Events.Consumers.System; using Jellyfin.Server.Implementations.Events.Consumers.Updates; using Jellyfin.Server.Implementations.Events.Consumers.Users; using MediaBrowser.Common.Updates; -using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Events.Authentication; using MediaBrowser.Controller.Events.Session; using MediaBrowser.Controller.Events.Updates; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -35,8 +34,8 @@ namespace Jellyfin.Server.Implementations.Events collection.AddScoped<IEventConsumer<SubtitleDownloadFailureEventArgs>, SubtitleDownloadFailureLogger>(); // Security consumers - collection.AddScoped<IEventConsumer<GenericEventArgs<AuthenticationRequest>>, AuthenticationFailedLogger>(); - collection.AddScoped<IEventConsumer<GenericEventArgs<AuthenticationResult>>, AuthenticationSucceededLogger>(); + collection.AddScoped<IEventConsumer<AuthenticationRequestEventArgs>, AuthenticationFailedLogger>(); + collection.AddScoped<IEventConsumer<AuthenticationResultEventArgs>, AuthenticationSucceededLogger>(); // Session consumers collection.AddScoped<IEventConsumer<PlaybackStartEventArgs>, PlaybackStartLogger>(); diff --git a/Jellyfin.Server.Implementations/Migrations/20230526173516_RemoveEasyPassword.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20230526173516_RemoveEasyPassword.Designer.cs new file mode 100644 index 000000000..00ccd9f0f --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20230526173516_RemoveEasyPassword.Designer.cs @@ -0,0 +1,650 @@ +// <auto-generated /> +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDbContext))] + [Migration("20230526173516_RemoveEasyPassword")] + partial class RemoveEasyPassword + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.5"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property<double>("EndHour") + .HasColumnType("REAL"); + + b.Property<double>("StartHour") + .HasColumnType("REAL"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<string>("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<int>("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<string>("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<string>("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<string>("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<string>("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.Property<string>("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property<string>("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<string>("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<bool>("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property<int?>("IndexBy") + .HasColumnType("INTEGER"); + + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<int>("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property<bool>("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property<bool>("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property<int>("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property<int>("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property<string>("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property<int>("Order") + .HasColumnType("INTEGER"); + + b.Property<int>("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<DateTime>("LastModified") + .HasColumnType("TEXT"); + + b.Property<string>("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<int?>("IndexBy") + .HasColumnType("INTEGER"); + + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<bool>("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property<bool>("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property<string>("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property<int>("SortOrder") + .HasColumnType("INTEGER"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.Property<int>("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("Kind") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + + b.Property<bool>("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("Kind") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + + b.Property<string>("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<string>("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property<string>("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateModified") + .HasColumnType("TEXT"); + + b.Property<string>("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<string>("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property<bool>("IsActive") + .HasColumnType("INTEGER"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("CustomName") + .HasColumnType("TEXT"); + + b.Property<string>("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property<string>("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<string>("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<bool>("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property<bool>("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property<bool>("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property<long>("InternalId") + .HasColumnType("INTEGER"); + + b.Property<int>("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property<DateTime?>("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property<int?>("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property<int>("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property<int?>("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property<bool>("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property<string>("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property<string>("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<bool>("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property<bool>("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property<bool>("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property<int?>("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<string>("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<int>("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property<int>("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property<string>("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20230526173516_RemoveEasyPassword.cs b/Jellyfin.Server.Implementations/Migrations/20230526173516_RemoveEasyPassword.cs new file mode 100644 index 000000000..9496ff3c0 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20230526173516_RemoveEasyPassword.cs @@ -0,0 +1,164 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// <inheritdoc /> + public partial class RemoveEasyPassword : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "EasyPassword", + schema: "jellyfin", + table: "Users"); + + migrationBuilder.RenameTable( + name: "Users", + schema: "jellyfin", + newName: "Users"); + + migrationBuilder.RenameTable( + name: "Preferences", + schema: "jellyfin", + newName: "Preferences"); + + migrationBuilder.RenameTable( + name: "Permissions", + schema: "jellyfin", + newName: "Permissions"); + + migrationBuilder.RenameTable( + name: "ItemDisplayPreferences", + schema: "jellyfin", + newName: "ItemDisplayPreferences"); + + migrationBuilder.RenameTable( + name: "ImageInfos", + schema: "jellyfin", + newName: "ImageInfos"); + + migrationBuilder.RenameTable( + name: "HomeSection", + schema: "jellyfin", + newName: "HomeSection"); + + migrationBuilder.RenameTable( + name: "DisplayPreferences", + schema: "jellyfin", + newName: "DisplayPreferences"); + + migrationBuilder.RenameTable( + name: "Devices", + schema: "jellyfin", + newName: "Devices"); + + migrationBuilder.RenameTable( + name: "DeviceOptions", + schema: "jellyfin", + newName: "DeviceOptions"); + + migrationBuilder.RenameTable( + name: "CustomItemDisplayPreferences", + schema: "jellyfin", + newName: "CustomItemDisplayPreferences"); + + migrationBuilder.RenameTable( + name: "ApiKeys", + schema: "jellyfin", + newName: "ApiKeys"); + + migrationBuilder.RenameTable( + name: "ActivityLogs", + schema: "jellyfin", + newName: "ActivityLogs"); + + migrationBuilder.RenameTable( + name: "AccessSchedules", + schema: "jellyfin", + newName: "AccessSchedules"); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "jellyfin"); + + migrationBuilder.RenameTable( + name: "Users", + newName: "Users", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "Preferences", + newName: "Preferences", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "Permissions", + newName: "Permissions", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "ItemDisplayPreferences", + newName: "ItemDisplayPreferences", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "ImageInfos", + newName: "ImageInfos", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "HomeSection", + newName: "HomeSection", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "DisplayPreferences", + newName: "DisplayPreferences", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "Devices", + newName: "Devices", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "DeviceOptions", + newName: "DeviceOptions", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "CustomItemDisplayPreferences", + newName: "CustomItemDisplayPreferences", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "ApiKeys", + newName: "ApiKeys", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "ActivityLogs", + newName: "ActivityLogs", + newSchema: "jellyfin"); + + migrationBuilder.RenameTable( + name: "AccessSchedules", + newName: "AccessSchedules", + newSchema: "jellyfin"); + + migrationBuilder.AddColumn<string>( + name: "EasyPassword", + schema: "jellyfin", + table: "Users", + type: "TEXT", + maxLength: 65535, + nullable: true); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index dd5f7f012..d23508096 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -15,9 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder - .HasDefaultSchema("jellyfin") - .HasAnnotation("ProductVersion", "6.0.9"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.5"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -41,7 +39,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId"); - b.ToTable("AccessSchedules", "jellyfin"); + b.ToTable("AccessSchedules"); }); modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => @@ -89,7 +87,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("DateCreated"); - b.ToTable("ActivityLogs", "jellyfin"); + b.ToTable("ActivityLogs"); }); modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => @@ -121,7 +119,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId", "ItemId", "Client", "Key") .IsUnique(); - b.ToTable("CustomItemDisplayPreferences", "jellyfin"); + b.ToTable("CustomItemDisplayPreferences"); }); modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => @@ -178,7 +176,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId", "ItemId", "Client") .IsUnique(); - b.ToTable("DisplayPreferences", "jellyfin"); + b.ToTable("DisplayPreferences"); }); modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => @@ -200,7 +198,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("DisplayPreferencesId"); - b.ToTable("HomeSection", "jellyfin"); + b.ToTable("HomeSection"); }); modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => @@ -225,7 +223,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId") .IsUnique(); - b.ToTable("ImageInfos", "jellyfin"); + b.ToTable("ImageInfos"); }); modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => @@ -269,7 +267,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId"); - b.ToTable("ItemDisplayPreferences", "jellyfin"); + b.ToTable("ItemDisplayPreferences"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => @@ -300,7 +298,7 @@ namespace Jellyfin.Server.Implementations.Migrations .IsUnique() .HasFilter("[UserId] IS NOT NULL"); - b.ToTable("Permissions", "jellyfin"); + b.ToTable("Permissions"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => @@ -333,7 +331,7 @@ namespace Jellyfin.Server.Implementations.Migrations .IsUnique() .HasFilter("[UserId] IS NOT NULL"); - b.ToTable("Preferences", "jellyfin"); + b.ToTable("Preferences"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => @@ -362,7 +360,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("AccessToken") .IsUnique(); - b.ToTable("ApiKeys", "jellyfin"); + b.ToTable("ApiKeys"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => @@ -420,7 +418,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId", "DeviceId"); - b.ToTable("Devices", "jellyfin"); + b.ToTable("Devices"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => @@ -441,7 +439,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("DeviceId") .IsUnique(); - b.ToTable("DeviceOptions", "jellyfin"); + b.ToTable("DeviceOptions"); }); modelBuilder.Entity("Jellyfin.Data.Entities.User", b => @@ -465,10 +463,6 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property<bool>("DisplayMissingEpisodes") .HasColumnType("INTEGER"); - b.Property<string>("EasyPassword") - .HasMaxLength(65535) - .HasColumnType("TEXT"); - b.Property<bool>("EnableAutoLogin") .HasColumnType("INTEGER"); @@ -554,7 +548,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("Username") .IsUnique(); - b.ToTable("Users", "jellyfin"); + b.ToTable("Users"); }); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => diff --git a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs index 72f3d6e8e..cb2d09a67 100644 --- a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs +++ b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Authentication; @@ -39,14 +40,18 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc /> // This is the version that we need to use for local users. Because reasons. - public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser) + public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User? resolvedUser) { - if (resolvedUser is null) + [DoesNotReturn] + static void ThrowAuthenticationException() { - throw new AuthenticationException("Specified user does not exist."); + throw new AuthenticationException("Invalid username or password"); } - bool success = false; + if (resolvedUser is null) + { + ThrowAuthenticationException(); + } // As long as jellyfin supports password-less users, we need this little block here to accommodate if (!HasPassword(resolvedUser) && string.IsNullOrEmpty(password)) @@ -60,15 +65,13 @@ namespace Jellyfin.Server.Implementations.Users // Handle the case when the stored password is null, but the user tried to login with a password if (resolvedUser.Password is null) { - throw new AuthenticationException("Invalid username or password"); + ThrowAuthenticationException(); } PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password); - success = _cryptographyProvider.Verify(readyHash, password); - - if (!success) + if (!_cryptographyProvider.Verify(readyHash, password)) { - throw new AuthenticationException("Invalid username or password"); + ThrowAuthenticationException(); } // Migrate old hashes to the new default diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs index 960195467..cefbd0624 100644 --- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs +++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs @@ -114,8 +114,6 @@ namespace Jellyfin.Server.Implementations.Users await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false); } - user.EasyPassword = pin; - return new ForgotPasswordResult { Action = ForgotPasswordAction.PinCode, diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index c4756433e..94ac4798c 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -20,7 +20,6 @@ using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Users; using Microsoft.EntityFrameworkCore; @@ -31,11 +30,10 @@ namespace Jellyfin.Server.Implementations.Users /// <summary> /// Manages the creation and retrieval of <see cref="User"/> instances. /// </summary> - public class UserManager : IUserManager + public partial class UserManager : IUserManager { private readonly IDbContextFactory<JellyfinDbContext> _dbProvider; private readonly IEventManager _eventManager; - private readonly ICryptoProvider _cryptoProvider; private readonly INetworkManager _networkManager; private readonly IApplicationHost _appHost; private readonly IImageProcessor _imageProcessor; @@ -53,7 +51,6 @@ namespace Jellyfin.Server.Implementations.Users /// </summary> /// <param name="dbProvider">The database provider.</param> /// <param name="eventManager">The event manager.</param> - /// <param name="cryptoProvider">The cryptography provider.</param> /// <param name="networkManager">The network manager.</param> /// <param name="appHost">The application host.</param> /// <param name="imageProcessor">The image processor.</param> @@ -61,7 +58,6 @@ namespace Jellyfin.Server.Implementations.Users public UserManager( IDbContextFactory<JellyfinDbContext> dbProvider, IEventManager eventManager, - ICryptoProvider cryptoProvider, INetworkManager networkManager, IApplicationHost appHost, IImageProcessor imageProcessor, @@ -69,7 +65,6 @@ namespace Jellyfin.Server.Implementations.Users { _dbProvider = dbProvider; _eventManager = eventManager; - _cryptoProvider = cryptoProvider; _networkManager = networkManager; _appHost = appHost; _imageProcessor = imageProcessor; @@ -105,6 +100,12 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public IEnumerable<Guid> UsersIds => _users.Keys; + // 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 ('), periods (.) and spaces ( ) + [GeneratedRegex("^[\\w\\ \\-'._@]+$")] + private static partial Regex ValidUsernameRegex(); + /// <inheritdoc/> public User? GetUserById(Guid id) { @@ -269,36 +270,15 @@ namespace Jellyfin.Server.Implementations.Users } /// <inheritdoc/> - public Task ResetEasyPassword(User user) - { - return ChangeEasyPassword(user, string.Empty, null); - } - - /// <inheritdoc/> public async Task ChangePassword(User user, string newPassword) { ArgumentNullException.ThrowIfNull(user); - - await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); - await UpdateUserAsync(user).ConfigureAwait(false); - - await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(user)).ConfigureAwait(false); - } - - /// <inheritdoc/> - public async Task ChangeEasyPassword(User user, string newPassword, string? newPasswordSha1) - { - if (newPassword is not null) - { - newPasswordSha1 = _cryptoProvider.CreatePasswordHash(newPassword).ToString(); - } - - if (string.IsNullOrWhiteSpace(newPasswordSha1)) + if (user.HasPermission(PermissionKind.IsAdministrator) && string.IsNullOrWhiteSpace(newPassword)) { - throw new ArgumentNullException(nameof(newPasswordSha1)); + throw new ArgumentException("Admin user passwords must not be empty", nameof(newPassword)); } - user.EasyPassword = newPasswordSha1; + await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); await UpdateUserAsync(user).ConfigureAwait(false); await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(user)).ConfigureAwait(false); @@ -315,7 +295,6 @@ namespace Jellyfin.Server.Implementations.Users ServerId = _appHost.SystemId, HasPassword = hasPassword, HasConfiguredPassword = hasPassword, - HasConfiguredEasyPassword = !string.IsNullOrEmpty(user.EasyPassword), EnableAutoLogin = user.EnableAutoLogin, LastLoginDate = user.LastLoginDate, LastActivityDate = user.LastActivityDate, @@ -400,7 +379,7 @@ namespace Jellyfin.Server.Implementations.Users } var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); - var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint) + var authResult = await AuthenticateLocalUser(username, password, user) .ConfigureAwait(false); var authenticationProvider = authResult.AuthenticationProvider; var success = authResult.Success; @@ -549,7 +528,7 @@ namespace Jellyfin.Server.Implementations.Users } var defaultName = Environment.UserName; - if (string.IsNullOrWhiteSpace(defaultName) || !IsValidUsername(defaultName)) + if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName)) { defaultName = "MyJellyfinUser"; } @@ -732,7 +711,7 @@ namespace Jellyfin.Server.Implementations.Users internal static void ThrowIfInvalidUsername(string name) { - if (!string.IsNullOrWhiteSpace(name) && IsValidUsername(name)) + if (!string.IsNullOrWhiteSpace(name) && ValidUsernameRegex().IsMatch(name)) { return; } @@ -740,14 +719,6 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)", nameof(name)); } - private static bool IsValidUsername(ReadOnlySpan<char> 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 ('), periods (.) and spaces ( ) - return Regex.IsMatch(name, @"^[\w\ \-'._@]+$"); - } - private IAuthenticationProvider GetAuthenticationProvider(User user) { return GetAuthenticationProviders(user)[0]; @@ -811,8 +782,7 @@ namespace Jellyfin.Server.Implementations.Users private async Task<(IAuthenticationProvider? AuthenticationProvider, string Username, bool Success)> AuthenticateLocalUser( string username, string password, - User? user, - string remoteEndPoint) + User? user) { bool success = false; IAuthenticationProvider? authenticationProvider = null; @@ -832,16 +802,6 @@ namespace Jellyfin.Server.Implementations.Users } } - if (!success - && _networkManager.IsInLocalNetwork(remoteEndPoint) - && user?.EnableLocalPassword == true - && !string.IsNullOrEmpty(user.EasyPassword)) - { - // Check easy password - var passwordHash = PasswordHash.Parse(user.EasyPassword); - success = _cryptoProvider.Verify(passwordHash, password); - } - return (authenticationProvider, username, success); } @@ -867,7 +827,7 @@ namespace Jellyfin.Server.Implementations.Users } catch (AuthenticationException ex) { - _logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name); + _logger.LogDebug(ex, "Error authenticating with provider {Provider}", provider.Name); return (username, false); } |
