diff --git a/.github/workflows/build-dotnet.yml b/.github/workflows/build-dotnet.yml index 1395b38..ba99431 100644 --- a/.github/workflows/build-dotnet.yml +++ b/.github/workflows/build-dotnet.yml @@ -15,7 +15,7 @@ jobs: - name: "Setup .NET Core" uses: actions/setup-dotnet@v1 with: - dotnet-version: "5.0.x" + dotnet-version: "8.0.x" - name: "Install dependencies" run: dotnet restore diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c6ee551..6696a94 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -25,7 +25,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 5.0.x + dotnet-version: 8.0.x - name: Initialize CodeQL uses: github/codeql-action/init@v1 diff --git a/.gitignore b/.gitignore index 03c9b93..c43d422 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ bin/ obj/ +.idea/ +*.DotSettings.user .vs/ diff --git a/Jellyfin.Plugin.Keycloak/Configuration/PluginConfiguration.cs b/Jellyfin.Plugin.Keycloak/Configuration/PluginConfiguration.cs index 12a92e3..bc018d6 100644 --- a/Jellyfin.Plugin.Keycloak/Configuration/PluginConfiguration.cs +++ b/Jellyfin.Plugin.Keycloak/Configuration/PluginConfiguration.cs @@ -1,25 +1,94 @@ using MediaBrowser.Model.Plugins; -namespace Jellyfin.Plugin.Keycloak.Configuration +namespace Jellyfin.Plugin.Keycloak.Configuration; + +/// +/// The main plugin. +/// +public class PluginConfiguration : BasePluginConfiguration { - - public class PluginConfiguration : BasePluginConfiguration + /// + /// Initializes a new instance of the class. + /// + public PluginConfiguration() { - public bool CreateUser { get; set; } - public string AuthServerUrl { get; set; } - public string Realm { get; set; } - public string Resource { get; set; } - public string ClientSecret { get; set; } - - - public PluginConfiguration() - { - // set default options here - CreateUser = true; - AuthServerUrl = ""; - Realm = ""; - Resource = ""; - ClientSecret = ""; - } + // set default options here + this.Enabled = false; + this.CreateUser = true; + this.Enable2Fa = false; + this.AuthServerUrl = string.Empty; + this.Realm = "master"; + this.ClientId = string.Empty; + this.ClientSecret = string.Empty; + this.OAuthScope = string.Empty; + this.RolesTokenAttribute = string.Empty; + this.UsernameTokenAttribute = "preferred_username"; + this.EnableAllFolders = false; + this.EnabledFolders = System.Array.Empty(); } + + /// + /// Gets or sets a value indicating whether Keycloak authentication is enabled. + /// + public bool Enabled { get; set; } + + /// + /// Gets or sets a value indicating whether creation of new users that dont exist in Jellyfin is enabled. + /// + public bool CreateUser { get; set; } + + /// + /// Gets or sets a value indicating whether 2-factor authentication (Password+TOTP). + /// + public bool Enable2Fa { get; set; } + + /// + /// Gets or sets Keycloak server URL. + /// + public string AuthServerUrl { get; set; } + + /// + /// Gets or sets Keycloak server realm. + /// + public string Realm { get; set; } + + /// + /// Gets or sets Keycloak client ID. + /// + public string ClientId { get; set; } + + /// + /// Gets or sets Keycloak client secret. + /// + public string ClientSecret { get; set; } + + /// + /// Gets or sets Keycloak OAuth scope. + /// + public string OAuthScope { get; set; } + + /// + /// Gets or sets Keycloak username token attribute. + /// + public string UsernameTokenAttribute { get; set; } + + /// + /// Gets or sets Keycloak roles token attribute. + /// + public string RolesTokenAttribute { get; set; } + + /// + /// Gets or sets a value indicating whether users without a role are allowed to log in. + /// + public bool AllowUsersWithoutRole { get; set; } + + /// + /// Gets or sets a value indicating whether to enable access to all library folders. + /// + public bool EnableAllFolders { get; set; } + + /// + /// Gets or sets a list of folder Ids which are enabled for access by default. + /// + public string[] EnabledFolders { get; set; } } diff --git a/Jellyfin.Plugin.Keycloak/Configuration/configPage.html b/Jellyfin.Plugin.Keycloak/Configuration/configPage.html index c23d4e7..c11d588 100644 --- a/Jellyfin.Plugin.Keycloak/Configuration/configPage.html +++ b/Jellyfin.Plugin.Keycloak/Configuration/configPage.html @@ -8,33 +8,75 @@
+

Keycloak Authentication

+
+ +
+
+ +
You need to add the TOTP (6 digits) to the password when logging in
+
- + -
Base Keycloak auth URI
- + -
Keycloak Realm
- - -
Keycloak Resource/Client
+ +
-
Client Secret
+
+ + +
+
+ + +
Access token attribute with the list of roles. Seperate keys with a '.' if the role list is part of a nested object.
+
+
+ + +
Access token attribute with the username
+
+
+ +
+ + +
+
+
Enable access to certain libraries by default
+
Add library access to certain users by giving them the 'lib-<ID>' role
+

+
+
diff --git a/Jellyfin.Plugin.Keycloak/Jellyfin.Plugin.Keycloak.csproj b/Jellyfin.Plugin.Keycloak/Jellyfin.Plugin.Keycloak.csproj index bf05667..7204b0d 100644 --- a/Jellyfin.Plugin.Keycloak/Jellyfin.Plugin.Keycloak.csproj +++ b/Jellyfin.Plugin.Keycloak/Jellyfin.Plugin.Keycloak.csproj @@ -1,9 +1,15 @@ - net5.0 - 1.0.0.0 - 1.0.0.0 + net8.0 + 2.0.0.1 + 2.0.0.1 + true + true + enable + AllEnabledByDefault + ../jellyfin.ruleset + CA1819 @@ -16,6 +22,13 @@ + + + + + + + diff --git a/Jellyfin.Plugin.Keycloak/KeyCloakAuthenticationProviderPlugin.cs b/Jellyfin.Plugin.Keycloak/KeyCloakAuthenticationProviderPlugin.cs index 0f74286..8e2cc7a 100644 --- a/Jellyfin.Plugin.Keycloak/KeyCloakAuthenticationProviderPlugin.cs +++ b/Jellyfin.Plugin.Keycloak/KeyCloakAuthenticationProviderPlugin.cs @@ -1,10 +1,14 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; using System.Net.Http; using System.Text.Json; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Plugin.Keycloak.Configuration; using JWT.Builder; using MediaBrowser.Common; using MediaBrowser.Common.Net; @@ -16,150 +20,24 @@ using Newtonsoft.Json.Linq; namespace Jellyfin.Plugin.Keycloak { + /// + /// KeyCloak Authentication Provider Plugin. + /// public class KeyCloakAuthenticationProviderPlugin : IAuthenticationProvider { private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger _logger; private readonly IApplicationHost _applicationHost; - private IUserManager _userManager; + private string _twoFactorPattern = @"(.*)(\d{6})$"; - private bool CreateUser => Plugin.Instance.Configuration.CreateUser; - private String AuthServerUrl => Plugin.Instance.Configuration.AuthServerUrl; - private String Realm => Plugin.Instance.Configuration.Realm; - private String Resource => Plugin.Instance.Configuration.Resource; - private String ClientSecret => Plugin.Instance.Configuration.ClientSecret; - - private HttpClient GetHttpClient() - { - return _httpClientFactory.CreateClient(NamedClient.Default); - } - - private String TokenURI => $"{AuthServerUrl}/realms/{Realm}/protocol/openid-connect/token"; - - private async Task GetKeycloakUser(string username, string password) - { - var httpClient = GetHttpClient(); - var keyValues = new List>(); - keyValues.Add( new KeyValuePair("username", username)); - keyValues.Add( new KeyValuePair("password", password)); - keyValues.Add( new KeyValuePair("grant_type", "password")); - keyValues.Add( new KeyValuePair("client_id", Resource)); - if (!String.IsNullOrWhiteSpace(ClientSecret)) - { - keyValues.Add(new KeyValuePair("client_secret", ClientSecret)); - } - - var content = new FormUrlEncodedContent(keyValues); - var response = await httpClient.PostAsync(TokenURI, content).ConfigureAwait(false); - var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - var parsed = await JsonSerializer.DeserializeAsync(responseStream).ConfigureAwait(false); - if (parsed == null) - return null; - try - { - var jwtToken = JwtBuilder.Create().Decode>(parsed.access_token); - List perms = new List(); - try - { - var resourceAccess = (JObject)jwtToken["resource_access"]; - perms = ((JArray)(((JObject)resourceAccess[Resource])["roles"])).ToObject>(); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Could not parse permissions for resource {Resource}"); - } - - return new KeycloakUser {Username = username, Permissions = perms}; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error parsing jwt token"); - } - - return null; - } - - private async Task UpdateUserInfo(KeycloakUser keycloakUser, User jellyfinUser) - { - jellyfinUser.SetPermission(PermissionKind.IsDisabled, true); - jellyfinUser.SetPermission(PermissionKind.IsAdministrator, false); - jellyfinUser.SetPermission(PermissionKind.EnableContentDownloading, false); - foreach (string permission in keycloakUser.Permissions) - { - switch (permission) - { - case "administrator": - jellyfinUser.SetPermission(PermissionKind.IsAdministrator, true); - break; - case "allowed_access": - jellyfinUser.SetPermission(PermissionKind.IsDisabled, false); - break; - } - } - await _userManager.UpdateUserAsync(jellyfinUser).ConfigureAwait(false); - return jellyfinUser; - } - - public async Task Authenticate(string username, string password) - { - _userManager ??= _applicationHost.Resolve(); - User user = null; - try - { - user = _userManager.GetUserByName(username); - } - catch (Exception e) - { - _logger.LogWarning("User Manager could not find a user for Keycloak User, this may not be fatal", e); - } - - KeycloakUser keycloakUser = await GetKeycloakUser(username, password); - if (keycloakUser == null) - { - throw new AuthenticationException("Error completing Keycloak login. Invalid username or password."); - } - - if (user == null) - { - if (CreateUser) - { - _logger.LogInformation($"Creating user {username}"); - user = await _userManager.CreateUserAsync(username).ConfigureAwait(false); - user.AuthenticationProviderId = GetType().FullName; - await UpdateUserInfo(keycloakUser, user); - } - else - { - _logger.LogError("Keycloak User not configured for Jellyfin: {username}", username); - throw new AuthenticationException( - $"Automatic User Creation is disabled and there is no Jellyfin user for authorized Uid: {username}"); - } - - } - else - { - await UpdateUserInfo(keycloakUser, user); - } - if (user.HasPermission(PermissionKind.IsDisabled)) - { - // If the user no longer has permission to access revoke all sessions for this user - _logger.LogInformation($"{username} is disabled, revoking all sessions"); - var sessionHandler = _applicationHost.Resolve(); - sessionHandler.RevokeUserTokens(user.Id, null); - } - return new ProviderAuthenticationResult { Username = username}; - } - - public bool HasPassword(User user) - { - return true; - } - - public Task ChangePassword(User user, string newPassword) - { - throw new System.NotImplementedException(); - } - public KeyCloakAuthenticationProviderPlugin(IHttpClientFactory httpClientFactory, + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public KeyCloakAuthenticationProviderPlugin( + IHttpClientFactory httpClientFactory, IApplicationHost applicationHost, ILogger logger) { @@ -168,7 +46,242 @@ namespace Jellyfin.Plugin.Keycloak _applicationHost = applicationHost; } + private string TokenUri => $"{Cfg().AuthServerUrl}/realms/{Cfg().Realm}/protocol/openid-connect/token"; + + /// public string Name => "Keycloak-Authentication"; + + /// public bool IsEnabled => true; + + private HttpClient GetHttpClient() + { + return _httpClientFactory.CreateClient(NamedClient.Default); + } + + private PluginConfiguration Cfg() + { + return Plugin.Instance.Configuration; + } + + private async Task<(KeycloakUser?, bool)> GetKeycloakUser(string username, string password, string? totp) + { + var httpClient = GetHttpClient(); + var keyValues = new List> + { + new KeyValuePair("username", username), + new KeyValuePair("password", password), + new KeyValuePair("grant_type", "password"), + new KeyValuePair("client_id", Cfg().ClientId) + }; + if (!string.IsNullOrWhiteSpace(Cfg().ClientSecret)) + { + keyValues.Add(new KeyValuePair("client_secret", Cfg().ClientSecret)); + } + + if (!string.IsNullOrWhiteSpace(Cfg().OAuthScope)) + { + keyValues.Add(new KeyValuePair("scope", Cfg().OAuthScope)); + } + + if (!string.IsNullOrWhiteSpace(totp)) + { + keyValues.Add(new KeyValuePair("totp", totp)); + } + + var content = new FormUrlEncodedContent(keyValues); + var response = await httpClient.PostAsync(TokenUri, content).ConfigureAwait(false); + var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + + if (!response.IsSuccessStatusCode) + { + KeycloakErrorResponse? err = await JsonSerializer.DeserializeAsync(responseStream).ConfigureAwait(false); + if (err == null) + { + return (null, false); + } + else if (err.Error == "invalid_grant") + { + return (null, true); + } + else + { + _logger.LogError("Keycloak error {0}: {1}", err.Error, err.ErrorDescription); + return (null, err.Error == "invalid_grant"); + } + } + + KeycloakTokenResponse? parsed = await JsonSerializer.DeserializeAsync(responseStream).ConfigureAwait(false); + if (parsed == null) + { + _logger.LogError("Error parsing Keycloak token response"); + return (null, false); + } + + try + { + var jwtToken = JwtBuilder.Create().Decode>(parsed.AccessToken); + + Collection perms = new Collection(); + var permsObj = GetJwtAttribute(jwtToken, Cfg().RolesTokenAttribute); + if (permsObj != null) + { + perms = ((JArray)permsObj).ToObject>(); + } + + var usernameObj = GetJwtAttribute(jwtToken, Cfg().UsernameTokenAttribute); + if (usernameObj != null) + { + username = (string)usernameObj; + } + + KeycloakUser user = new KeycloakUser(username, perms); + + return (user, false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error parsing jwt token"); + } + + return (null, false); + } + + private async Task UpdateUserInfo(KeycloakUser keycloakUser, User? jellyfinUser) + { + var userManager = _applicationHost.Resolve(); + if (jellyfinUser != null) + { + jellyfinUser.SetPermission(PermissionKind.IsDisabled, !keycloakUser.IsEnabled); + jellyfinUser.SetPermission(PermissionKind.IsAdministrator, keycloakUser.IsAdmin); + jellyfinUser.SetPermission(PermissionKind.EnableCollectionManagement, keycloakUser.IsEditor); + jellyfinUser.SetPermission(PermissionKind.EnableSubtitleManagement, keycloakUser.IsEditor); + jellyfinUser.SetPermission(PermissionKind.EnableAllFolders, Cfg().EnableAllFolders || keycloakUser.IsAdmin); + + var enabledFolders = new HashSet(Cfg().EnabledFolders); + enabledFolders.UnionWith(keycloakUser.EnabledFolders); + jellyfinUser.SetPreference(PreferenceKind.EnabledFolders, enabledFolders.ToArray()); + + await userManager.UpdateUserAsync(jellyfinUser).ConfigureAwait(false); + } + } + + private object? GetJwtAttribute(IDictionary jwt, string tokenAttribute) + { + if (!string.IsNullOrWhiteSpace(tokenAttribute)) + { + var parts = tokenAttribute.Split('.'); + try + { + IDictionary subObj = jwt; + for (int i = 0; i < parts.Length - 1; i++) + { + subObj = (IDictionary)subObj[parts[i]]; + } + + return subObj[parts[^1]]; + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not extract {TokenAttribute} from JWT", tokenAttribute); + } + } + + return null; + } + + /// + public async Task Authenticate(string username, string password) + { + _logger.LogInformation("Keycloak login: {Username}", username); + var userManager = _applicationHost.Resolve(); + string password_t = password; + string? totp = null; + if (Cfg().Enable2Fa) + { + var match = Regex.Match(password, _twoFactorPattern); + if (match.Success) + { + password_t = match.Groups[1].Value; + totp = match.Groups[2].Value; + } + } + + (KeycloakUser? keycloakUser, bool retryNoOtp) = await GetKeycloakUser(username, password_t, totp).ConfigureAwait(false); + if (keycloakUser == null && totp != null && retryNoOtp) + { + (keycloakUser, _) = await GetKeycloakUser(username, password, null).ConfigureAwait(false); + } + + if (keycloakUser == null) + { + throw new AuthenticationException("Error completing Keycloak login. Invalid username or password."); + } + + keycloakUser.IsEnabled |= Cfg().AllowUsersWithoutRole; + + User? user = null; + try + { + user = userManager.GetUserByName(keycloakUser.Username); + } + catch (Exception e) + { + _logger.LogWarning("User Manager could not find a user for Keycloak User, this may not be fatal: {E}", e); + } + + if (user == null) + { + if (!keycloakUser.IsEnabled) + { + _logger.LogError("Keycloak User not allowed to use Jellyfin: {Username}", username); + throw new AuthenticationException("User is not allowed to use Jellyfin"); + } + else if (Cfg().CreateUser) + { + _logger.LogInformation("Creating user: {Username}", username); + user = await userManager.CreateUserAsync(username).ConfigureAwait(false); + var userAuthenticationProviderId = GetType().FullName; + if (userAuthenticationProviderId != null) + { + user.AuthenticationProviderId = userAuthenticationProviderId; + } + + await UpdateUserInfo(keycloakUser, user).ConfigureAwait(false); + } + else + { + _logger.LogError("Keycloak User not configured for Jellyfin: {Username}", username); + throw new AuthenticationException( + $"Automatic User Creation is disabled and there is no Jellyfin user for authorized Uid: {username}"); + } + } + else + { + await UpdateUserInfo(keycloakUser, user).ConfigureAwait(false); + } + + if (user != null && user.HasPermission(PermissionKind.IsDisabled)) + { + // If the user no longer has permission to access revoke all sessions for this user + _logger.LogInformation("{Username} is disabled, revoking all sessions", username); + var sessionHandler = _applicationHost.Resolve(); + await sessionHandler.RevokeUserTokens(user.Id, null).ConfigureAwait(false); + } + + return new ProviderAuthenticationResult { Username = username }; + } + + /// + public bool HasPassword(User user) + { + return true; + } + + /// + public Task ChangePassword(User user, string newPassword) + { + throw new NotImplementedException(); + } } } diff --git a/Jellyfin.Plugin.Keycloak/KeycloakAccessToken.cs b/Jellyfin.Plugin.Keycloak/KeycloakAccessToken.cs deleted file mode 100644 index dcc8b45..0000000 --- a/Jellyfin.Plugin.Keycloak/KeycloakAccessToken.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System.Collections.Generic; - -namespace Jellyfin.Plugin.Keycloak -{ - -} diff --git a/Jellyfin.Plugin.Keycloak/KeycloakErrorResponse.cs b/Jellyfin.Plugin.Keycloak/KeycloakErrorResponse.cs new file mode 100644 index 0000000..b810ff1 --- /dev/null +++ b/Jellyfin.Plugin.Keycloak/KeycloakErrorResponse.cs @@ -0,0 +1,24 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.Keycloak +{ + /// + /// Response model for Keycloak errors. + /// + [DataContract] + public class KeycloakErrorResponse + { + /// + /// Gets or sets error code. + /// + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// + /// Gets or sets error description. + /// + [JsonPropertyName("error_description")] + public string? ErrorDescription { get; set; } + } +} diff --git a/Jellyfin.Plugin.Keycloak/KeycloakTokenResponse.cs b/Jellyfin.Plugin.Keycloak/KeycloakTokenResponse.cs index 89e586b..63fcd82 100644 --- a/Jellyfin.Plugin.Keycloak/KeycloakTokenResponse.cs +++ b/Jellyfin.Plugin.Keycloak/KeycloakTokenResponse.cs @@ -1,17 +1,54 @@ using System.Runtime.Serialization; +using System.Text.Json.Serialization; namespace Jellyfin.Plugin.Keycloak { + /// + /// Response model for the keycloak token. + /// [DataContract] public class KeycloakTokenResponse { - public string access_token { get; set; } - public int expires_in { get; set; } - [DataMember(Name = "not-before-policy")] - public int not_before_policy { get; set; } - public string refresh_token { get; set; } - public string scope { get; set; } - public string session_state { get; set; } - public string token_type { get; set; } + /// + /// Gets or sets the access token instance. + /// + [JsonPropertyName("access_token")] + public string? AccessToken { get; set; } + + /// + /// Gets or sets the expiry time instance. + /// + [JsonPropertyName("expires_in")] + public int ExpiresIn { get; set; } + + /// + /// Gets or sets the not-before-policy instance. + /// + [JsonPropertyName("not-before-policy")] + public int NotBeforePolicy { get; set; } + + /// + /// Gets or sets the refresh token instance. + /// + [JsonPropertyName("refresh_token")] + public string? RefreshToken { get; set; } + + /// + /// Gets or sets the scope instance. + /// + [JsonPropertyName("scope")] + public string? Scope { get; set; } + + /// + /// Gets or sets the session state instance. + /// + [JsonPropertyName("session_state")] + public string? SessionState { get; set; } + + /// + /// Gets or sets the token type instance. + /// + [JsonPropertyName("token_type")] + public string? TokenType { get; set; } } } diff --git a/Jellyfin.Plugin.Keycloak/KeycloakUser.cs b/Jellyfin.Plugin.Keycloak/KeycloakUser.cs index 65bf82d..0c7b578 100644 --- a/Jellyfin.Plugin.Keycloak/KeycloakUser.cs +++ b/Jellyfin.Plugin.Keycloak/KeycloakUser.cs @@ -1,10 +1,72 @@ using System.Collections.Generic; +using System.Collections.ObjectModel; namespace Jellyfin.Plugin.Keycloak { + /// + /// User Model for Keycloak Users. + /// public class KeycloakUser { - public string Username { get; set; } - public List Permissions { get; set; } + /// + /// Initializes a new instance of the class. + /// + /// Instance of the username of the user. + /// User roles. + public KeycloakUser(string username, Collection roles) + { + Username = username; + Roles = roles; + + IsAdmin = false; + IsEditor = false; + IsEnabled = false; + var enabledFolders = new List(); + + foreach (string permission in Roles) + { + IsAdmin |= permission == "admin"; + IsEditor |= permission == "editor"; + IsEnabled |= permission == "user"; + + if (permission.StartsWith("lib-", System.StringComparison.Ordinal)) + { + enabledFolders.Add(permission[4..]); + } + } + + IsEnabled |= IsAdmin || IsEditor; + EnabledFolders = enabledFolders.ToArray(); + } + + /// + /// Gets the value of the username of an user. + /// + public string Username { get; } + + /// + /// Gets the value of roles of an user. + /// + public Collection Roles { get; } + + /// + /// Gets a list of the enabled media libraries of an user. + /// + public string[] EnabledFolders { get; } + + /// + /// Gets a value indicating whether the user is an administrator. + /// + public bool IsAdmin { get; } + + /// + /// Gets a value indicating whether the user is an editor. + /// + public bool IsEditor { get; } + + /// + /// Gets or sets a value indicating whether the user should be enabled. + /// + public bool IsEnabled { get; set; } } } diff --git a/Jellyfin.Plugin.Keycloak/Plugin.cs b/Jellyfin.Plugin.Keycloak/Plugin.cs index 6fb1811..cf529d0 100644 --- a/Jellyfin.Plugin.Keycloak/Plugin.cs +++ b/Jellyfin.Plugin.Keycloak/Plugin.cs @@ -8,19 +8,33 @@ using MediaBrowser.Model.Serialization; namespace Jellyfin.Plugin.Keycloak { + /// + /// Keycloak Plugin. + /// public class Plugin : BasePlugin, IHasWebPages { - public override string Name => "Keycloak-Auth"; - - public override Guid Id => Guid.Parse("40886866-b3dd-4d6a-bf9b-25c83e6c3d10"); - + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) { Instance = this; } - public static Plugin Instance { get; private set; } + /// + /// Gets the plugin instance. + /// + public static Plugin Instance { get; private set; } = null!; + /// + public override string Name => "Keycloak-Auth"; + + /// + public override Guid Id => Guid.Parse("40886866-b3dd-4d6a-bf9b-25c83e6c3d10"); + + /// public IEnumerable GetPages() { return new[] @@ -28,7 +42,7 @@ namespace Jellyfin.Plugin.Keycloak new PluginPageInfo { Name = this.Name, - EmbeddedResourcePath = string.Format("{0}.Configuration.configPage.html", GetType().Namespace) + EmbeddedResourcePath = $"{GetType().Namespace}.Configuration.configPage.html" } }; } diff --git a/Jellyfin.Plugin.Keycloak/ServiceRegistrator.cs b/Jellyfin.Plugin.Keycloak/ServiceRegistrator.cs new file mode 100644 index 0000000..50afcff --- /dev/null +++ b/Jellyfin.Plugin.Keycloak/ServiceRegistrator.cs @@ -0,0 +1,18 @@ +using MediaBrowser.Controller; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Plugins; +using Microsoft.Extensions.DependencyInjection; + +namespace Jellyfin.Plugin.Keycloak; + +/// +/// Register LDAP services. +/// +public class ServiceRegistrator : IPluginServiceRegistrator +{ + /// + public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost) + { + serviceCollection.AddSingleton(); + } +} diff --git a/jellyfin.ruleset b/jellyfin.ruleset new file mode 100644 index 0000000..e7bc7ec --- /dev/null +++ b/jellyfin.ruleset @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +