Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add profile image synchronization #154

Merged
merged 2 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions LDAP-Auth/Api/Models/LdapUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public LdapUser()
{
LinkedJellyfinUserId = Guid.Empty;
LdapUid = string.Empty;
ProfileImageHash = string.Empty;
}

/// <summary>
Expand All @@ -25,5 +26,10 @@ public LdapUser()
/// Gets or sets the LDAP Uid associated with the user.
/// </summary>
public string LdapUid { get; set; }

/// <summary>
/// Gets or sets the profile image hash. This is used to detect if the profile image provided by LDAP changed.
/// </summary>
public string ProfileImageHash { get; set; }
}
}
18 changes: 16 additions & 2 deletions LDAP-Auth/Config/PluginConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public PluginConfiguration()
LdapUidAttribute = "uid";
LdapUsernameAttribute = "cn";
LdapPasswordAttribute = "userPassword";
EnableLdapProfileImageSync = false;
LdapProfileImageAttribute = "jpegphoto";
EnableAllFolders = false;
EnabledFolders = Array.Empty<string>();

Expand Down Expand Up @@ -153,6 +155,16 @@ public PluginConfiguration()
/// </summary>
public string LdapPasswordAttribute { get; set; }

/// <summary>
/// Gets or sets a value indicating whether profile images are synchronized from LDAP.
/// </summary>
public bool EnableLdapProfileImageSync { get; set; }

/// <summary>
/// Gets or sets the ldap profile image attribute.
/// </summary>
public string LdapProfileImageAttribute { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to enable access to all library folders.
/// </summary>
Expand All @@ -173,7 +185,8 @@ public PluginConfiguration()
/// </summary>
/// <param name="userGuid">The user Guid.</param>
/// <param name="ldapUid">The LDAP UID associated with the user.</param>
public void AddUser(Guid userGuid, string ldapUid)
/// <param name="profileImageHash">The hash of the profile image provided by LDAP.</param>
public void AddUser(Guid userGuid, string ldapUid, string profileImageHash)
{
// Ensure we do not have more than one entry for a given user
// This may happen if a user tries to authenticate after their
Expand All @@ -185,7 +198,8 @@ public void AddUser(Guid userGuid, string ldapUid)
var ldapUser = new LdapUser
{
LinkedJellyfinUserId = userGuid,
LdapUid = ldapUid
LdapUid = ldapUid,
ProfileImageHash = profileImageHash
};
ldapUsers.Add(ldapUser);
LdapUsers = ldapUsers.ToArray();
Expand Down
19 changes: 19 additions & 0 deletions LDAP-Auth/Config/configPage.html
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,19 @@ <h4 style="margin-top:-0.3em">Users</h4>
<input is="emby-input" type="text" id="txtLdapPasswordAttribute" label="LDAP Password Attribute:" />
<div class="fieldDescription">The LDAP attribute for the user password; only required if Allow Password Change is enabled.</div>
</div>
<div class="inputContainer fldExternalAddressFilter">
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableProfileImageSync" />
<span>Enable profile image synchronization</span>
</label>
<div class="fieldDescription checkboxFieldDescription">
Note that this is a one way sync. If a user changes their profile image in Jellyfin it won't be updated unless the profile image changes in LDAP or the user deletes it in Jellyfin.
</div>
</div>
<div class="inputContainer fldExternalAddressFilter">
<input is="emby-input" type="text" id="txtLdapProfileImageAttribute" label="LDAP Profile Image Attribute:" />
<div class="fieldDescription">The LDAP attribute for synchronizing profile images.</div>
</div>
<hr>
<h4>Administrators</h4>
<div class="inputContainer fldExternalAddressFilter">
Expand Down Expand Up @@ -230,6 +243,8 @@ <h2>${HeaderLibraryAccess}</h2>
txtLdapUidAttribute: document.querySelector("#txtLdapUidAttribute"),
txtLdapUsernameAttribute: document.querySelector("#txtLdapUsernameAttribute"),
txtLdapPasswordAttribute: document.querySelector("#txtLdapPasswordAttribute"),
chkEnableProfileImageSync: document.querySelector("#chkEnableProfileImageSync"),
txtLdapProfileImageAttribute: document.querySelector("#txtLdapProfileImageAttribute"),
chkEnableAllFolders: document.querySelector('#chkEnableAllFolders'),
folderAccessList: document.querySelector('.folderAccess'),
txtPasswordResetUrl: document.querySelector("#txtLdapPasswordResetUrl")
Expand Down Expand Up @@ -264,6 +279,8 @@ <h2>${HeaderLibraryAccess}</h2>
LdapConfigurationPage.txtLdapUidAttribute.value = config.LdapUidAttribute;
LdapConfigurationPage.txtLdapUsernameAttribute.value = config.LdapUsernameAttribute;
LdapConfigurationPage.txtLdapPasswordAttribute.value = config.LdapPasswordAttribute;
LdapConfigurationPage.chkEnableProfileImageSync.checked = config.EnableLdapProfileImageSync;
LdapConfigurationPage.txtLdapProfileImageAttribute.value = config.LdapProfileImageAttribute;
config.EnableAllFolders = config.EnableAllFolders || false;
LdapConfigurationPage.chkEnableAllFolders.checked = config.EnableAllFolders;
/* Default to empty array if Enabled Folders is not set */
Expand Down Expand Up @@ -351,6 +368,8 @@ <h2>${HeaderLibraryAccess}</h2>
config.LdapUidAttribute = LdapConfigurationPage.txtLdapUidAttribute.value;
config.LdapUsernameAttribute = LdapConfigurationPage.txtLdapUsernameAttribute.value;
config.LdapPasswordAttribute = LdapConfigurationPage.txtLdapPasswordAttribute.value;
config.EnableLdapProfileImageSync = LdapConfigurationPage.chkEnableProfileImageSync.checked;
config.LdapProfileImageAttribute = LdapConfigurationPage.txtLdapProfileImageAttribute.value;
/* Map the set of checked input items to an array of library Id's */
config.EnableAllFolders = LdapConfigurationPage.chkEnableAllFolders.checked || false;
let folders = document.querySelectorAll('#folderList input');
Expand Down
38 changes: 38 additions & 0 deletions LDAP-Auth/Helpers/ProfileImageUpdater.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.IO;
using System.Net.Mime;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Providers;

namespace Jellyfin.Plugin.LDAP_Auth.Helpers
{
/// <summary>
/// Provides utility methods to update the profile image of a user.
/// </summary>
public static class ProfileImageUpdater
{
/// <summary>
/// Sets the profile image of a user to the provided image.
/// </summary>
/// <param name="user">The user to update.</param>
/// <param name="ldapProfileImage">The data representing the profile image to set.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface, used to retrieve the path to save the profile picture.</param>
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface, used to save the profile picture.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static async Task SetProfileImage(
User user,
byte[] ldapProfileImage,
IServerConfigurationManager serverConfigurationManager,
IProviderManager providerManager)
{
var userDataPath = Path.Combine(serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
user.ProfileImage = new ImageInfo(Path.Combine(userDataPath, "profile.jpg"));

using var profileImageMemoryStream = new MemoryStream(ldapProfileImage);
await providerManager
.SaveImage(profileImageMemoryStream, MediaTypeNames.Image.Jpeg, user.ProfileImage.Path)
.ConfigureAwait(false);
}
}
}
32 changes: 28 additions & 4 deletions LDAP-Auth/LDAPAuthenticationProviderPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
Expand All @@ -12,7 +13,9 @@
using Jellyfin.Plugin.LDAP_Auth.Helpers;
using MediaBrowser.Common;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Users;
using Microsoft.Extensions.Logging;
using Novell.Directory.Ldap;
Expand Down Expand Up @@ -44,6 +47,10 @@ public LdapAuthenticationProviderPlugin(IApplicationHost applicationHost, ILogge

private string UsernameAttr => LdapPlugin.Instance.Configuration.LdapUsernameAttribute;

private bool EnableProfileImageSync => LdapPlugin.Instance.Configuration.EnableLdapProfileImageSync;

private string ProfileImageAttr => LdapPlugin.Instance.Configuration.LdapProfileImageAttribute;

private string SearchFilter => LdapPlugin.Instance.Configuration.LdapSearchFilter;

private string AdminFilter => LdapPlugin.Instance.Configuration.LdapAdminFilter;
Expand Down Expand Up @@ -105,7 +112,7 @@ public async Task<ProviderAuthenticationResult> Authenticate(string username, st
else
{
// Add the user to our Ldap users
LdapPlugin.Instance.Configuration.AddUser(user.Id, ldapUid);
LdapPlugin.Instance.Configuration.AddUser(user.Id, ldapUid, string.Empty);
LdapPlugin.Instance.SaveConfiguration();
}
}
Expand Down Expand Up @@ -192,10 +199,21 @@ public async Task<ProviderAuthenticationResult> Authenticate(string username, st
user.SetPreference(PreferenceKind.EnabledFolders, LdapPlugin.Instance.Configuration.EnabledFolders);
}

var providerManager = _applicationHost.Resolve<IProviderManager>();
var serverConfigurationManager = _applicationHost.Resolve<IServerConfigurationManager>();
var ldapProfileImage = GetAttribute(ldapUser, ProfileImageAttr)?.ByteValue;
var ldapProfileImageHash = string.Empty;
if (ldapProfileImage is not null && EnableProfileImageSync)
{
ldapProfileImageHash = Convert.ToBase64String(MD5.HashData(ldapProfileImage));

await ProfileImageUpdater.SetProfileImage(user, ldapProfileImage, serverConfigurationManager, providerManager).ConfigureAwait(false);
}

await userManager.UpdateUserAsync(user).ConfigureAwait(false);

// Add the user to our Ldap users
LdapPlugin.Instance.Configuration.AddUser(user.Id, ldapUid);
LdapPlugin.Instance.Configuration.AddUser(user.Id, ldapUid, ldapProfileImageHash);
LdapPlugin.Instance.SaveConfiguration();
}
else
Expand Down Expand Up @@ -455,7 +473,7 @@ public LdapEntry LocateLdapUser(string username)
LdapPlugin.Instance.Configuration.LdapBaseDn,
LdapConnection.ScopeSub,
realSearchFilter,
new[] { UsernameAttr, UidAttr },
new[] { UsernameAttr, UidAttr, ProfileImageAttr },
false);
}
catch (LdapException e)
Expand Down Expand Up @@ -512,7 +530,13 @@ public Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
throw new NotImplementedException();
}

private LdapAttribute GetAttribute(LdapEntry userEntry, string attr)
/// <summary>
/// Retrieves the requested attribute from a <see cref="LdapEntry" />.
/// </summary>
/// <param name="userEntry">The <see cref="LdapEntry" /> to retrieve the attribute from.</param>
/// <param name="attr">The attribute to retrieve from the <see cref="LdapEntry" />.</param>
/// <returns>The value of the <see cref="LdapEntry" /> or null if it does not exist.</returns>
public LdapAttribute GetAttribute(LdapEntry userEntry, string attr)
{
var attributeSet = userEntry.GetAttributeSet();
if (attributeSet.ContainsKey(attr))
Expand Down
Loading