aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server.Implementations
diff options
context:
space:
mode:
authorNick <20588554+nicknsy@users.noreply.github.com>2023-10-18 19:27:05 -0700
committerNick <20588554+nicknsy@users.noreply.github.com>2023-10-18 19:27:05 -0700
commitcd662506a1f63f9b20e7f5caa9b671eb3d71ea5a (patch)
treeb58f7158e21e7ed21d77b0f0abfce23d796b3fe3 /Jellyfin.Server.Implementations
parentc7feea27fde8af60984c8fe41444dc245dbde395 (diff)
parentde08d38a6f2a6e773fa1000574e08322605b56d3 (diff)
Merge branch 'master' into trickplay
Diffstat (limited to 'Jellyfin.Server.Implementations')
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs11
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs13
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs21
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs5
-rw-r--r--Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs10
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs654
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs29
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs6
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthorizationContext.cs30
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs21
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs53
11 files changed, 767 insertions, 86 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..3f3a0dec5 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
@@ -1,9 +1,8 @@
using System.Globalization;
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 +11,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 +28,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..9626817e9 100644
--- a/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs
+++ b/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs
@@ -1,5 +1,4 @@
-using Jellyfin.Data.Events;
-using Jellyfin.Data.Events.System;
+using Jellyfin.Data.Events.System;
using Jellyfin.Data.Events.Users;
using Jellyfin.Server.Implementations.Events.Consumers.Library;
using Jellyfin.Server.Implementations.Events.Consumers.Security;
@@ -8,12 +7,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 +33,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/20230923170422_UserCastReceiver.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs
new file mode 100644
index 000000000..2884d4256
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs
@@ -0,0 +1,654 @@
+// <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("20230923170422_UserCastReceiver")]
+ partial class UserCastReceiver
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
+
+ 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<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .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/20230923170422_UserCastReceiver.cs b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs
new file mode 100644
index 000000000..f06410c15
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs
@@ -0,0 +1,29 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class UserCastReceiver : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<string>(
+ name: "CastReceiverId",
+ table: "Users",
+ type: "TEXT",
+ maxLength: 32,
+ nullable: true);
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "CastReceiverId",
+ table: "Users");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index 3c06e1cfc..f725ababe 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "7.0.7");
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@@ -488,6 +488,10 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasMaxLength(255)
.HasColumnType("TEXT");
+ b.Property<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
b.Property<bool>("DisplayCollectionsView")
.HasColumnType("INTEGER");
diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
index 700e63970..77f8f7071 100644
--- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
@@ -49,14 +49,13 @@ namespace Jellyfin.Server.Implementations.Security
/// <summary>
/// Gets the authorization.
/// </summary>
- /// <param name="httpReq">The HTTP req.</param>
+ /// <param name="httpContext">The HTTP context.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
- private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpReq)
+ private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpContext)
{
- var auth = GetAuthorizationDictionary(httpReq);
- var authInfo = await GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query).ConfigureAwait(false);
+ var authInfo = await GetAuthorizationInfo(httpContext.Request).ConfigureAwait(false);
- httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
+ httpContext.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
return authInfo;
}
@@ -80,7 +79,6 @@ namespace Jellyfin.Server.Implementations.Security
auth.TryGetValue("Token", out token);
}
-#pragma warning disable CA1508 // string.IsNullOrEmpty(token) is always false.
if (string.IsNullOrEmpty(token))
{
token = headers["X-Emby-Token"];
@@ -118,7 +116,6 @@ namespace Jellyfin.Server.Implementations.Security
// Request doesn't contain a token.
return authInfo;
}
-#pragma warning restore CA1508
authInfo.HasToken = true;
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
@@ -219,24 +216,7 @@ namespace Jellyfin.Server.Implementations.Security
/// <summary>
/// Gets the auth.
/// </summary>
- /// <param name="httpReq">The HTTP req.</param>
- /// <returns>Dictionary{System.StringSystem.String}.</returns>
- private static Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
- {
- var auth = httpReq.Request.Headers["X-Emby-Authorization"];
-
- if (string.IsNullOrEmpty(auth))
- {
- auth = httpReq.Request.Headers[HeaderNames.Authorization];
- }
-
- return auth.Count > 0 ? GetAuthorization(auth[0]) : null;
- }
-
- /// <summary>
- /// Gets the auth.
- /// </summary>
- /// <param name="httpReq">The HTTP req.</param>
+ /// <param name="httpReq">The HTTP request.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
{
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/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 1d03baa4c..b2cb589f7 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -15,12 +15,12 @@ using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
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 +31,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;
@@ -45,6 +44,7 @@ namespace Jellyfin.Server.Implementations.Users
private readonly InvalidAuthProvider _invalidAuthProvider;
private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider;
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IDictionary<Guid, User> _users;
@@ -53,27 +53,27 @@ 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>
/// <param name="logger">The logger.</param>
+ /// <param name="serverConfigurationManager">The system config manager.</param>
public UserManager(
IDbContextFactory<JellyfinDbContext> dbProvider,
IEventManager eventManager,
- ICryptoProvider cryptoProvider,
INetworkManager networkManager,
IApplicationHost appHost,
IImageProcessor imageProcessor,
- ILogger<UserManager> logger)
+ ILogger<UserManager> logger,
+ IServerConfigurationManager serverConfigurationManager)
{
_dbProvider = dbProvider;
_eventManager = eventManager;
- _cryptoProvider = cryptoProvider;
_networkManager = networkManager;
_appHost = appHost;
_imageProcessor = imageProcessor;
_logger = logger;
+ _serverConfigurationManager = serverConfigurationManager;
_passwordResetProviders = appHost.GetExports<IPasswordResetProvider>();
_authenticationProviders = appHost.GetExports<IAuthenticationProvider>();
@@ -105,6 +105,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)
{
@@ -287,6 +293,7 @@ namespace Jellyfin.Server.Implementations.Users
public UserDto GetUserDto(User user, string? remoteEndPoint = null)
{
var hasPassword = GetAuthenticationProvider(user).HasPassword(user);
+ var castReceiverApplications = _serverConfigurationManager.Configuration.CastReceiverApplications;
return new UserDto
{
Name = user.Username,
@@ -314,7 +321,11 @@ namespace Jellyfin.Server.Implementations.Users
OrderedViews = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews),
GroupedFolders = user.GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders),
MyMediaExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes),
- LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes)
+ LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes),
+ CastReceiverId = string.IsNullOrEmpty(user.CastReceiverId)
+ ? castReceiverApplications.FirstOrDefault()?.Id
+ : castReceiverApplications.FirstOrDefault(c => string.Equals(c.Id, user.CastReceiverId, StringComparison.Ordinal))?.Id
+ ?? castReceiverApplications.FirstOrDefault()?.Id
},
Policy = new UserPolicy
{
@@ -378,7 +389,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;
@@ -527,7 +538,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";
}
@@ -603,6 +614,13 @@ namespace Jellyfin.Server.Implementations.Users
user.RememberSubtitleSelections = config.RememberSubtitleSelections;
user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
+ // Only set cast receiver id if it is passed in and it exists in the server config.
+ if (!string.IsNullOrEmpty(config.CastReceiverId)
+ && _serverConfigurationManager.Configuration.CastReceiverApplications.Any(c => string.Equals(c.Id, config.CastReceiverId, StringComparison.Ordinal)))
+ {
+ user.CastReceiverId = config.CastReceiverId;
+ }
+
user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
@@ -710,7 +728,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;
}
@@ -718,14 +736,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];
@@ -789,8 +799,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;
@@ -835,7 +844,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);
}