Merge branch 'feature/ldap' of github.com:dpraul/Ombi into work/ldap-integration

This commit is contained in:
Paannda 2021-03-29 22:15:33 +00:00
commit 49b4cf8286
25 changed files with 730 additions and 25 deletions

View file

@ -0,0 +1,168 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Novell.Directory.Ldap;
using Ombi.Core.Settings;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
namespace Ombi.Core.Authentication
{
/// <summary>
/// Ldap Authentication Provider
/// </summary>
public class LdapUserManager : ILdapUserManager
{
public LdapUserManager(ILogger<LdapUserManager> logger, ISettingsService<LdapSettings> ldapSettings)
{
_ldapSettingsService = ldapSettings;
_logger = logger;
}
private readonly ISettingsService<LdapSettings> _ldapSettingsService;
private readonly ILogger<LdapUserManager> _logger;
public async Task<LdapSettings> GetSettings()
{
return await _ldapSettingsService.GetSettingsAsync();
}
public async Task<OmbiUser> LdapEntryToOmbiUser(LdapEntry entry)
{
var settings = await GetSettings();
var userName = GetLdapAttribute(entry, settings.UsernameAttribute).StringValue;
return new OmbiUser
{
UserType = UserType.LdapUser,
ProviderUserId = entry.Dn,
UserName = userName
};
}
private LdapAttribute GetLdapAttribute(LdapEntry userEntry, string attr)
{
try
{
return userEntry.GetAttribute(attr);
}
catch (Exception)
{
return null;
}
}
private async Task<LdapConnection> BindLdapConnection(string username, string password)
{
var settings = await GetSettings();
var ldapClient = new LdapConnection { SecureSocketLayer = settings.UseSsl };
try
{
if (settings.SkipSslVerify)
{
ldapClient.UserDefinedServerCertValidationDelegate += LdapClient_UserDefinedServerCertValidationDelegate;
}
ldapClient.Connect(settings.Hostname, settings.Port);
if (settings.UseStartTls)
{
ldapClient.StartTls();
}
ldapClient.Bind(username, password);
}
catch (Exception e)
{
_logger.LogError(e, "Failed to Connect or Bind to server");
throw e;
}
finally
{
ldapClient.UserDefinedServerCertValidationDelegate -= LdapClient_UserDefinedServerCertValidationDelegate;
}
if (!ldapClient.Bound)
{
ldapClient.Dispose();
return null;
}
return ldapClient;
}
private async Task<ILdapSearchResults> SearchLdapUsers(LdapConnection ldapClient)
{
var settings = await GetSettings();
string[] searchAttributes = { settings.UsernameAttribute };
_logger.LogDebug("Search: {1} {2} @ {3}", settings.BaseDn, settings.SearchFilter, settings.Hostname);
return ldapClient.Search(settings.BaseDn, LdapConnection.ScopeSub, settings.SearchFilter, searchAttributes, false);
}
public async Task<ILdapSearchResults> GetLdapUsers()
{
var settings = await GetSettings();
using var ldapClient = await BindLdapConnection(settings.BindUserDn, settings.BindUserPassword);
return await SearchLdapUsers(ldapClient);
}
public async Task<LdapEntry> LocateLdapUser(string username)
{
var settings = await GetSettings();
using var ldapClient = await BindLdapConnection(settings.BindUserDn, settings.BindUserPassword);
var ldapUsers = await SearchLdapUsers(ldapClient);
if (ldapUsers == null)
{
return null;
}
while (ldapUsers.HasMore())
{
var currentUser = ldapUsers.Next();
var foundUsername = GetLdapAttribute(currentUser, settings.UsernameAttribute)?.StringValue;
if (foundUsername == username)
{
return currentUser;
}
}
return null;
}
/// <summary>
/// Authenticate user against the ldap server.
/// </summary>
/// <param name="user">Username to authenticate.</param>
/// <param name="password">Password to authenticate.</param>
public async Task<bool> Authenticate(OmbiUser user, string password)
{
var ldapUser = await LocateLdapUser(user.UserName);
if (ldapUser == null)
{
return false;
}
try
{
using var ldapClient = await BindLdapConnection(ldapUser.Dn, password);
return (bool)ldapClient?.Bound;
} catch (Exception)
{
return false;
}
}
private static bool LdapClient_UserDefinedServerCertValidationDelegate(
object sender,
System.Security.Cryptography.X509Certificates.X509Certificate certificate,
System.Security.Cryptography.X509Certificates.X509Chain chain,
System.Net.Security.SslPolicyErrors sslPolicyErrors)
=> true;
}
}

View file

@ -26,12 +26,14 @@
#endregion #endregion
using System; using System;
using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Novell.Directory.Ldap;
using Ombi.Api.Emby; using Ombi.Api.Emby;
using Ombi.Api.Jellyfin; using Ombi.Api.Jellyfin;
using Ombi.Api.Plex; using Ombi.Api.Plex;
@ -50,9 +52,14 @@ namespace Ombi.Core.Authentication
IPasswordHasher<OmbiUser> passwordHasher, IEnumerable<IUserValidator<OmbiUser>> userValidators, IPasswordHasher<OmbiUser> passwordHasher, IEnumerable<IUserValidator<OmbiUser>> userValidators,
IEnumerable<IPasswordValidator<OmbiUser>> passwordValidators, ILookupNormalizer keyNormalizer, IEnumerable<IPasswordValidator<OmbiUser>> passwordValidators, ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<OmbiUser>> logger, IPlexApi plexApi, IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<OmbiUser>> logger, IPlexApi plexApi,
<<<<<<< HEAD
IEmbyApiFactory embyApi, ISettingsService<EmbySettings> embySettings, IEmbyApiFactory embyApi, ISettingsService<EmbySettings> embySettings,
IJellyfinApiFactory jellyfinApi, ISettingsService<JellyfinSettings> jellyfinSettings, IJellyfinApiFactory jellyfinApi, ISettingsService<JellyfinSettings> jellyfinSettings,
ISettingsService<AuthenticationSettings> auth) ISettingsService<AuthenticationSettings> auth)
=======
IEmbyApiFactory embyApi, ISettingsService<EmbySettings> embySettings, ISettingsService<AuthenticationSettings> auth,
ILdapUserManager ldapUserManager, ISettingsService<UserManagementSettings> userManagementSettings)
>>>>>>> 691c70804f203fab858b1079a1cf3d5e4adbf322
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
{ {
_plexApi = plexApi; _plexApi = plexApi;
@ -61,14 +68,49 @@ namespace Ombi.Core.Authentication
_embySettings = embySettings; _embySettings = embySettings;
_jellyfinSettings = jellyfinSettings; _jellyfinSettings = jellyfinSettings;
_authSettings = auth; _authSettings = auth;
_ldapUserManager = ldapUserManager;
_userManagementSettings = userManagementSettings;
} }
private readonly IPlexApi _plexApi; private readonly IPlexApi _plexApi;
private readonly IEmbyApiFactory _embyApi; private readonly IEmbyApiFactory _embyApi;
<<<<<<< HEAD
private readonly IJellyfinApiFactory _jellyfinApi; private readonly IJellyfinApiFactory _jellyfinApi;
=======
private readonly ILdapUserManager _ldapUserManager;
>>>>>>> 691c70804f203fab858b1079a1cf3d5e4adbf322
private readonly ISettingsService<EmbySettings> _embySettings; private readonly ISettingsService<EmbySettings> _embySettings;
private readonly ISettingsService<JellyfinSettings> _jellyfinSettings; private readonly ISettingsService<JellyfinSettings> _jellyfinSettings;
private readonly ISettingsService<AuthenticationSettings> _authSettings; private readonly ISettingsService<AuthenticationSettings> _authSettings;
private readonly ISettingsService<UserManagementSettings> _userManagementSettings;
public async Task<OmbiUser> FindUser(string userName)
{
var user = await FindByNameAsync(userName);
if (user != null)
{
return user;
}
user = await FindByEmailAsync(userName);
if (user != null)
{
user.EmailLogin = true;
return user;
}
var ldapSettings = await _ldapUserManager.GetSettings();
if (!(ldapSettings.IsEnabled && ldapSettings.CreateUsersAtLogin))
{
return null;
}
var ldapUser = await _ldapUserManager.LocateLdapUser(userName);
if (ldapUser == null)
{
return null;
}
return await CreateOmbiUserFromLdapEntry(ldapUser);
}
public override async Task<bool> CheckPasswordAsync(OmbiUser user, string password) public override async Task<bool> CheckPasswordAsync(OmbiUser user, string password)
{ {
@ -90,9 +132,15 @@ namespace Ombi.Core.Authentication
{ {
return await CheckEmbyPasswordAsync(user, password); return await CheckEmbyPasswordAsync(user, password);
} }
<<<<<<< HEAD
if (user.UserType == UserType.JellyfinUser) if (user.UserType == UserType.JellyfinUser)
{ {
return await CheckJellyfinPasswordAsync(user, password); return await CheckJellyfinPasswordAsync(user, password);
=======
if (user.UserType == UserType.LdapUser)
{
return await CheckLdapPasswordAsync(user, password);
>>>>>>> 691c70804f203fab858b1079a1cf3d5e4adbf322
} }
return false; return false;
} }
@ -227,5 +275,41 @@ namespace Ombi.Core.Authentication
} }
return false; return false;
} }
private async Task<bool> CheckLdapPasswordAsync(OmbiUser user, string password)
{
var ldapSettings = await _ldapUserManager.GetSettings();
if (!ldapSettings.IsEnabled)
{
return false;
}
return await _ldapUserManager.Authenticate(user, password);
}
public async Task<OmbiUser> CreateOmbiUserFromLdapEntry(LdapEntry entry)
{
var newUser = await _ldapUserManager.LdapEntryToOmbiUser(entry);
var userManagementSettings = await _userManagementSettings.GetSettingsAsync();
var result = await CreateAsync(newUser);
if (!result.Succeeded)
{
foreach (var identityError in result.Errors)
{
Logger.LogError(LoggingEvents.Authentication, identityError.Description);
}
return null;
}
if (userManagementSettings.DefaultRoles.Any())
{
foreach (var defaultRole in userManagementSettings.DefaultRoles)
{
await AddToRoleAsync(newUser, defaultRole);
}
}
return newUser;
}
} }
} }

View file

@ -0,0 +1,20 @@
using System.Threading.Tasks;
using Novell.Directory.Ldap;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
namespace Ombi.Core.Authentication
{
public interface ILdapUserManager
{
Task<LdapSettings> GetSettings();
Task<bool> Authenticate(OmbiUser user, string password);
Task<ILdapSearchResults> GetLdapUsers();
Task<LdapEntry> LocateLdapUser(string username);
Task<OmbiUser> LdapEntryToOmbiUser(LdapEntry entry);
}
}

View file

@ -16,6 +16,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.0" />
<PackageReference Include="MusicBrainzAPI" Version="2.0.1" /> <PackageReference Include="MusicBrainzAPI" Version="2.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Novell.Directory.Ldap.NETStandard" Version="3.3.1" />
<PackageReference Include="System.Diagnostics.Process" Version="4.3.0" /> <PackageReference Include="System.Diagnostics.Process" Version="4.3.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Core" Version="1.1.0" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Core" Version="1.1.0" />
</ItemGroup> </ItemGroup>

View file

@ -52,6 +52,7 @@ using Ombi.Schedule.Jobs.Jellyfin;
using Ombi.Schedule.Jobs.Ombi; using Ombi.Schedule.Jobs.Ombi;
using Ombi.Schedule.Jobs.Plex; using Ombi.Schedule.Jobs.Plex;
using Ombi.Schedule.Jobs.Sonarr; using Ombi.Schedule.Jobs.Sonarr;
using Ombi.Schedule.Jobs.Ldap;
using Ombi.Store.Repository.Requests; using Ombi.Store.Repository.Requests;
using Ombi.Updater; using Ombi.Updater;
using Ombi.Api.Telegram; using Ombi.Api.Telegram;
@ -101,6 +102,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IMusicSender, MusicSender>(); services.AddTransient<IMusicSender, MusicSender>();
services.AddTransient<IMassEmailSender, MassEmailSender>(); services.AddTransient<IMassEmailSender, MassEmailSender>();
services.AddTransient<IPlexOAuthManager, PlexOAuthManager>(); services.AddTransient<IPlexOAuthManager, PlexOAuthManager>();
services.AddTransient<ILdapUserManager, LdapUserManager>();
services.AddTransient<IVoteEngine, VoteEngine>(); services.AddTransient<IVoteEngine, VoteEngine>();
services.AddTransient<IDemoMovieSearchEngine, DemoMovieSearchEngine>(); services.AddTransient<IDemoMovieSearchEngine, DemoMovieSearchEngine>();
services.AddTransient<IDemoTvSearchEngine, DemoTvSearchEngine>(); services.AddTransient<IDemoTvSearchEngine, DemoTvSearchEngine>();
@ -231,6 +233,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IPlexUserImporter, PlexUserImporter>(); services.AddTransient<IPlexUserImporter, PlexUserImporter>();
services.AddTransient<IEmbyUserImporter, EmbyUserImporter>(); services.AddTransient<IEmbyUserImporter, EmbyUserImporter>();
services.AddTransient<IJellyfinUserImporter, JellyfinUserImporter>(); services.AddTransient<IJellyfinUserImporter, JellyfinUserImporter>();
services.AddTransient<ILdapUserImporter, LdapUserImporter>();
services.AddTransient<IWelcomeEmail, WelcomeEmail>(); services.AddTransient<IWelcomeEmail, WelcomeEmail>();
services.AddTransient<ICouchPotatoSync, CouchPotatoSync>(); services.AddTransient<ICouchPotatoSync, CouchPotatoSync>();
services.AddTransient<IProcessProvider, ProcessProvider>(); services.AddTransient<IProcessProvider, ProcessProvider>();

View file

@ -0,0 +1,7 @@

namespace Ombi.Schedule.Jobs.Ldap
{
public interface ILdapUserImporter : IBaseJob
{
}
}

View file

@ -0,0 +1,87 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Authentication;
using Ombi.Core.Settings;
using Ombi.Hubs;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Quartz;
namespace Ombi.Schedule.Jobs.Ldap
{
public class LdapUserImporter : ILdapUserImporter
{
public LdapUserImporter(OmbiUserManager userManager, ILdapUserManager ldapUserManager,
ISettingsService<LdapSettings> ldapSettings, ISettingsService<UserManagementSettings> ums, IHubContext<NotificationHub> notification)
{
_userManager = userManager;
_ldapUserManager = ldapUserManager;
_ldapSettings = ldapSettings;
_userManagementSettings = ums;
_notification = notification;
}
private readonly OmbiUserManager _userManager;
private readonly ILdapUserManager _ldapUserManager;
private readonly ISettingsService<LdapSettings> _ldapSettings;
private readonly ISettingsService<UserManagementSettings> _userManagementSettings;
private readonly IHubContext<NotificationHub> _notification;
public async Task Execute(IJobExecutionContext job)
{
var userManagementSettings = await _userManagementSettings.GetSettingsAsync();
if (!userManagementSettings.ImportLdapUsers)
{
return;
}
var settings = await _ldapSettings.GetSettingsAsync();
if (!settings.IsEnabled)
{
return;
}
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
.SendAsync(NotificationHub.NotificationEvent, "LDAP User Importer Started");
var allUsers = await _userManager.Users.Where(x => x.UserType == UserType.LdapUser).ToListAsync();
var allLdapUsers = await _ldapUserManager.GetLdapUsers();
while (allLdapUsers.HasMore())
{
var currentUser = allLdapUsers.Next();
var existingEmbyUser = allUsers.FirstOrDefault(x => x.ProviderUserId == currentUser.Dn);
if (existingEmbyUser != null)
{
continue;
}
await _userManager.CreateOmbiUserFromLdapEntry(currentUser);
}
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
.SendAsync(NotificationHub.NotificationEvent, "LDAP User Importer Finished");
}
private bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
_userManager?.Dispose();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

View file

@ -8,6 +8,7 @@ using Ombi.Schedule.Jobs;
using Ombi.Schedule.Jobs.Couchpotato; using Ombi.Schedule.Jobs.Couchpotato;
using Ombi.Schedule.Jobs.Emby; using Ombi.Schedule.Jobs.Emby;
using Ombi.Schedule.Jobs.Jellyfin; using Ombi.Schedule.Jobs.Jellyfin;
using Ombi.Schedule.Jobs.Ldap;
using Ombi.Schedule.Jobs.Lidarr; using Ombi.Schedule.Jobs.Lidarr;
using Ombi.Schedule.Jobs.Ombi; using Ombi.Schedule.Jobs.Ombi;
using Ombi.Schedule.Jobs.Plex; using Ombi.Schedule.Jobs.Plex;
@ -56,6 +57,7 @@ namespace Ombi.Schedule
await AddDvrApps(s); await AddDvrApps(s);
await AddSystem(s); await AddSystem(s);
await AddNotifications(s); await AddNotifications(s);
await AddLdap(s);
// Run Quartz // Run Quartz
await OmbiQuartz.Start(); await OmbiQuartz.Start();
@ -113,5 +115,9 @@ namespace Ombi.Schedule
{ {
await OmbiQuartz.Instance.AddJob<INotificationService>(nameof(INotificationService), "Notifications", null); await OmbiQuartz.Instance.AddJob<INotificationService>(nameof(INotificationService), "Notifications", null);
} }
private static async Task AddLdap(JobSettings s)
{
await OmbiQuartz.Instance.AddJob<ILdapUserImporter>(nameof(ILdapUserImporter), "LDAP", JobSettingsHelper.UserImporter(s));
}
} }
} }

View file

@ -0,0 +1,81 @@
namespace Ombi.Settings.Settings.Models
{
public class LdapSettings : Settings
{
public LdapSettings()
{
IsEnabled = false;
CreateUsersAtLogin = true;
Hostname = "ldap-server.example.tld";
BaseDn = "o=domains,dc=example,dc=tld";
Port = 389;
UsernameAttribute = "uid";
SearchFilter = "(memberOf=cn=Users,dc=example,dc=tld)";
BindUserDn = "cn=BindUser,dc=example,dc=tld";
BindUserPassword = "password";
UseSsl = true;
UseStartTls = false;
SkipSslVerify = false;
}
/// <summary>
/// Gets or sets whether LDAP is enabled.
/// </summary>
public bool IsEnabled { get; set; }
/// <summary>
/// Gets or sets whether users should be automatically created at login.
/// </summary>
public bool CreateUsersAtLogin { get; set; }
/// <summary>
/// Gets or sets the ldap server ip or url.
/// </summary>
public string Hostname { get; set; }
/// <summary>
/// Gets or sets the ldap base search dn.
/// </summary>
public string BaseDn { get; set; }
/// <summary>
/// Gets or sets the ldap port.
/// </summary>
public int Port { get; set; }
/// <summary>
/// Gets or sets the ldap username attribute.
/// </summary>
public string UsernameAttribute { get; set; }
/// <summary>
/// Gets or sets the ldap user search filter.
/// </summary>
public string SearchFilter { get; set; }
/// <summary>
/// Gets or sets the ldap bind user dn.
/// </summary>
public string BindUserDn { get; set; }
/// <summary>
/// Gets or sets the ldap bind user password.
/// </summary>
public string BindUserPassword { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to use ssl when connecting to the ldap server.
/// </summary>
public bool UseSsl { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to use StartTls when connecting to the ldap server.
/// </summary>
public bool UseStartTls { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to skip ssl verification.
/// </summary>
public bool SkipSslVerify { get; set; }
}
}

View file

@ -8,6 +8,7 @@ namespace Ombi.Settings.Settings.Models
public bool ImportPlexUsers { get; set; } public bool ImportPlexUsers { get; set; }
public bool ImportEmbyUsers { get; set; } public bool ImportEmbyUsers { get; set; }
public bool ImportJellyfinUsers { get; set; } public bool ImportJellyfinUsers { get; set; }
public bool ImportLdapUsers { get; set; }
public int MovieRequestLimit { get; set; } public int MovieRequestLimit { get; set; }
public int EpisodeRequestLimit { get; set; } public int EpisodeRequestLimit { get; set; }
public string DefaultStreamingCountry { get; set; } = "US"; public string DefaultStreamingCountry { get; set; } = "US";

View file

@ -35,5 +35,6 @@ namespace Ombi.Store.Entities
EmbyUser = 3, EmbyUser = 3,
EmbyConnectUser = 4, EmbyConnectUser = 4,
JellyfinUser = 5, JellyfinUser = 5,
LdapUser = 6,
} }
} }

View file

@ -194,6 +194,21 @@ export interface IAuthenticationSettings extends ISettings {
enableOAuth: boolean; enableOAuth: boolean;
} }
export interface ILdapSettings extends ISettings {
isEnabled: boolean;
hostname: string;
port: number;
baseDn: string;
useSsl: boolean;
useStartTls: boolean;
skipSslVerify: boolean;
bindUserDn: string;
bindUserPassword: string;
usernameAttribute: string;
searchFilter: string;
createUsersAtLogin: boolean;
}
export interface ICustomPage extends ISettings { export interface ICustomPage extends ISettings {
enabled: boolean; enabled: boolean;
fontAwesomeIcon: string; fontAwesomeIcon: string;
@ -206,6 +221,7 @@ export interface IUserManagementSettings extends ISettings {
importPlexAdmin: boolean; importPlexAdmin: boolean;
importEmbyUsers: boolean; importEmbyUsers: boolean;
importJellyfinUsers: boolean; importJellyfinUsers: boolean;
importLdapUsers: boolean;
defaultRoles: string[]; defaultRoles: string[];
movieRequestLimit: number; movieRequestLimit: number;
episodeRequestLimit: number; episodeRequestLimit: number;

View file

@ -34,6 +34,10 @@ export class JobService extends ServiceHelpers {
public runJellyfinImporter(): Observable<boolean> { public runJellyfinImporter(): Observable<boolean> {
return this.http.post<boolean>(`${this.url}jellyfinUserImporter/`, {headers: this.headers}); return this.http.post<boolean>(`${this.url}jellyfinUserImporter/`, {headers: this.headers});
} }
public runLdapImporter(): Observable<boolean> {
return this.http.post<boolean>(`${this.url}ldapUserImporter/`, {headers: this.headers});
}
public runPlexCacher(): Observable<boolean> { public runPlexCacher(): Observable<boolean> {
return this.http.post<boolean>(`${this.url}plexcontentcacher/`, {headers: this.headers}); return this.http.post<boolean>(`${this.url}plexcontentcacher/`, {headers: this.headers});

View file

@ -39,6 +39,7 @@ import {
IVoteSettings, IVoteSettings,
ITwilioSettings, ITwilioSettings,
IWebhookNotificationSettings, IWebhookNotificationSettings,
ILdapSettings,
} from "../interfaces"; } from "../interfaces";
import { ServiceHelpers } from "./service.helpers"; import { ServiceHelpers } from "./service.helpers";
@ -133,6 +134,14 @@ export class SettingsService extends ServiceHelpers {
return this.http.post<boolean>(`${this.url}/Authentication`, JSON.stringify(settings), {headers: this.headers}); return this.http.post<boolean>(`${this.url}/Authentication`, JSON.stringify(settings), {headers: this.headers});
} }
public getLdap(): Observable<ILdapSettings> {
return this.http.get<ILdapSettings>(`${this.url}/ldap`, {headers: this.headers});
}
public saveLdap(settings: ILdapSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}/ldap`, JSON.stringify(settings), {headers: this.headers});
}
// Using http since we need it not to be authenticated to get the landing page settings // Using http since we need it not to be authenticated to get the landing page settings
public getLandingPage(): Observable<ILandingPageSettings> { public getLandingPage(): Observable<ILandingPageSettings> {
return this.http.get<ILandingPageSettings>(`${this.url}/LandingPage`, {headers: this.headers}); return this.http.get<ILandingPageSettings>(`${this.url}/LandingPage`, {headers: this.headers});

View file

@ -0,0 +1,78 @@
<settings-menu></settings-menu>
<fieldset *ngIf="form" class="small-middle-container">
<legend>LDAP Settings</legend>
<form
[formGroup]="form"
(ngSubmit)="onSubmit(form)"
class="md-form-field"
>
<div class="form-group">
<mat-checkbox formControlName="isEnabled">
LDAP Enabled
</mat-checkbox>
<mat-checkbox formControlName="createUsersAtLogin">
Create Users at Login
</mat-checkbox>
</div>
<div class="form-group">
<mat-form-field appearance="outline">
<mat-label>Hostname</mat-label>
<input matInput required formControlName="hostname">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Port</mat-label>
<input matInput type="number" required formControlName="port">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Base DN</mat-label>
<input matInput required formControlName="baseDn">
</mat-form-field>
<mat-checkbox formControlName="useSsl">
Use SSL
</mat-checkbox>
<mat-checkbox formControlName="useStartTls">
Use StartTLS
</mat-checkbox>
<mat-checkbox formControlName="skipSslVerify">
Skip SSL Verification
</mat-checkbox>
</div>
<div class="form-group">
<mat-form-field appearance="outline">
<mat-label>Bind User DN</mat-label>
<input matInput required formControlName="bindUserDn">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Bind User Password</mat-label>
<input matInput required type="password" formControlName="bindUserPassword">
</mat-form-field>
</div>
<div class="form-group">
<mat-form-field appearance="outline">
<mat-label>Username Attribute</mat-label>
<input matInput required formControlName="usernameAttribute">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Search Filter</mat-label>
<input matInput required formControlName="searchFilter">
</mat-form-field>
</div>
<div class="form-group">
<button
mat-raised-button
type="submit"
color="primary"
[disabled]="form.invalid"
>
Submit
</button>
</div>
</form>
</fieldset>

View file

@ -0,0 +1,9 @@
.small-middle-container {
margin: auto;
width: 95%;
margin-top: 10px;
}
mat-checkbox {
margin-right: 10px;
}

View file

@ -0,0 +1,62 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, FormGroup } from "@angular/forms";
import { Subscription } from 'rxjs';
import { NotificationService } from "../../services/notification.service";
import { SettingsService } from "../../services/settings.service";
@Component({
templateUrl: "./ldap.component.html",
styleUrls: ["./ldap.component.scss"],
})
export class LdapComponent implements OnInit, OnDestroy {
public form: FormGroup;
private sub: Subscription;
constructor(
private settingsService: SettingsService,
private notificationService: NotificationService,
private formBuilder: FormBuilder
) {}
public ngOnInit() {
this.sub = this.settingsService.getLdap().subscribe(ldapSettings => {
this.form = this.formBuilder.group({
isEnabled: [ldapSettings.isEnabled],
hostname: [ldapSettings.hostname],
port: [ldapSettings.port],
baseDn: [ldapSettings.baseDn],
useSsl: [ldapSettings.useSsl],
useStartTls: [ldapSettings.useStartTls],
skipSslVerify: [ldapSettings.skipSslVerify],
bindUserDn: [ldapSettings.bindUserDn],
bindUserPassword: [ldapSettings.bindUserPassword],
usernameAttribute: [ldapSettings.usernameAttribute],
searchFilter: [ldapSettings.searchFilter],
createUsersAtLogin: [ldapSettings.createUsersAtLogin],
});
});
}
public ngOnDestroy() {
this.sub.unsubscribe();
}
public onSubmit(form: FormGroup) {
if (form.invalid) {
this.notificationService.error("Please check your entered values");
return;
}
this.settingsService.saveLdap(form.value).subscribe(x => {
if (x) {
this.notificationService.success("Successfully saved LDAP settings");
} else {
this.notificationService.success("There was an error when saving LDAP settings");
}
});
}
}

View file

@ -48,6 +48,7 @@ import { UpdateComponent } from "./update/update.component";
import { UserManagementComponent } from "./usermanagement/usermanagement.component"; import { UserManagementComponent } from "./usermanagement/usermanagement.component";
import { VoteComponent } from "./vote/vote.component"; import { VoteComponent } from "./vote/vote.component";
import { WikiComponent } from "./wiki.component"; import { WikiComponent } from "./wiki.component";
import { LdapComponent } from "./ldap/ldap.component";
import { SettingsMenuComponent } from "./settingsmenu.component"; import { SettingsMenuComponent } from "./settingsmenu.component";
@ -97,6 +98,7 @@ const routes: Routes = [
{ path: "SickRage", component: SickRageComponent, canActivate: [AuthGuard] }, { path: "SickRage", component: SickRageComponent, canActivate: [AuthGuard] },
{ path: "Issues", component: IssuesComponent, canActivate: [AuthGuard] }, { path: "Issues", component: IssuesComponent, canActivate: [AuthGuard] },
{ path: "Authentication", component: AuthenticationComponent, canActivate: [AuthGuard] }, { path: "Authentication", component: AuthenticationComponent, canActivate: [AuthGuard] },
{ path: "Ldap", component: LdapComponent, canActivate: [AuthGuard] },
{ path: "Mobile", component: MobileComponent, canActivate: [AuthGuard] }, { path: "Mobile", component: MobileComponent, canActivate: [AuthGuard] },
{ path: "MassEmail", component: MassEmailComponent, canActivate: [AuthGuard] }, { path: "MassEmail", component: MassEmailComponent, canActivate: [AuthGuard] },
{ path: "Newsletter", component: NewsletterComponent, canActivate: [AuthGuard] }, { path: "Newsletter", component: NewsletterComponent, canActivate: [AuthGuard] },
@ -158,6 +160,7 @@ const routes: Routes = [
TelegramComponent, TelegramComponent,
IssuesComponent, IssuesComponent,
AuthenticationComponent, AuthenticationComponent,
LdapComponent,
MobileComponent, MobileComponent,
MassEmailComponent, MassEmailComponent,
NewsletterComponent, NewsletterComponent,

View file

@ -7,6 +7,7 @@
<button mat-menu-item [routerLink]="['/Settings/Issues']"><i class="fas fa-exclamation-triangle icon-spacing"></i> Issues</button> <button mat-menu-item [routerLink]="['/Settings/Issues']"><i class="fas fa-exclamation-triangle icon-spacing"></i> Issues</button>
<button mat-menu-item [routerLink]="['/Settings/UserManagement']"><i class="fas fa-users-cog icon-spacing"></i> User Management</button> <button mat-menu-item [routerLink]="['/Settings/UserManagement']"><i class="fas fa-users-cog icon-spacing"></i> User Management</button>
<button mat-menu-item [routerLink]="['/Settings/Authentication']"><i class="fas fa-sign-in-alt icon-spacing"></i> Authentication</button> <button mat-menu-item [routerLink]="['/Settings/Authentication']"><i class="fas fa-sign-in-alt icon-spacing"></i> Authentication</button>
<button mat-menu-item [routerLink]="['/Settings/Ldap']"><i class="fas fa-address-card icon-spacing"></i> Ldap</button>
<!-- <button mat-menu-item [routerLink]="['/Settings/Vote']">Vote</button> --> <!-- <button mat-menu-item [routerLink]="['/Settings/Vote']">Vote</button> -->
<button mat-menu-item [routerLink]="['/Settings/TheMovieDb']"><i class="fas fa-film icon-spacing"></i> The Movie Database</button> <button mat-menu-item [routerLink]="['/Settings/TheMovieDb']"><i class="fas fa-film icon-spacing"></i> The Movie Database</button>
</mat-menu> </mat-menu>

View file

@ -45,7 +45,12 @@
<p-autoComplete [(ngModel)]="bannedJellyfinUsers" [suggestions]="filteredJellyfinUsers" [multiple]="true" field="username" (completeMethod)="filterJellyfinList($event)"></p-autoComplete> <p-autoComplete [(ngModel)]="bannedJellyfinUsers" [suggestions]="filteredJellyfinUsers" [multiple]="true" field="username" (completeMethod)="filterJellyfinList($event)"></p-autoComplete>
</div> </div>
</div>
<div *ngIf="ldapEnabled">
<div class="form-group">
<mat-checkbox id="importLdapUsers" [(ngModel)]="settings.importLdapUsers">Import LDAP Users</mat-checkbox>
</div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
@ -92,7 +97,7 @@
</div> </div>
<div><button type="button" [disabled]="!enableImportButton" (click)="runImporter()" class="mat-focus-indicator mat-stroked-button mat-raised-button mat-button-base"> <div><button type="button" [disabled]="!enableImportButton" (click)="runImporter()" class="mat-focus-indicator mat-stroked-button mat-raised-button mat-button-base">
Run Importer<div matripple class="mat-ripple mat-button-ripple" ng-reflect-disabled="false" ng-reflect-centered="false" ng-reflect-trigger="[object HTMLButtonElement]"></div><div class="mat-button-focus-overlay"></div></button> Run Importer<div matripple class="mat-ripple mat-button-ripple" ng-reflect-disabled="false" ng-reflect-centered="false" ng-reflect-trigger="[object HTMLButtonElement]"></div><div class="mat-button-focus-overlay"></div></button>
</div> </div>
<div class="md-form-field" style="margin-top:1em;"></div> <div class="md-form-field" style="margin-top:1em;"></div>
</fieldset> </fieldset>
</div> </div>

View file

@ -12,7 +12,11 @@ export class UserManagementComponent implements OnInit {
public plexEnabled: boolean; public plexEnabled: boolean;
public embyEnabled: boolean; public embyEnabled: boolean;
<<<<<<< HEAD
public jellyfinEnabled: boolean; public jellyfinEnabled: boolean;
=======
public ldapEnabled: boolean;
>>>>>>> 691c70804f203fab858b1079a1cf3d5e4adbf322
public settings: IUserManagementSettings; public settings: IUserManagementSettings;
public claims: ICheckbox[]; public claims: ICheckbox[];
@ -45,7 +49,11 @@ export class UserManagementComponent implements OnInit {
this.settingsService.getUserManagementSettings().subscribe(x => { this.settingsService.getUserManagementSettings().subscribe(x => {
this.settings = x; this.settings = x;
<<<<<<< HEAD
if(x.importEmbyUsers || x.importJellyfinUsers || x.importPlexUsers) { if(x.importEmbyUsers || x.importJellyfinUsers || x.importPlexUsers) {
=======
if(x.importEmbyUsers || x.importPlexUsers || x.importLdapUsers) {
>>>>>>> 691c70804f203fab858b1079a1cf3d5e4adbf322
this.enableImportButton = true; this.enableImportButton = true;
} }
@ -100,7 +108,11 @@ export class UserManagementComponent implements OnInit {
}); });
this.settingsService.getPlex().subscribe(x => this.plexEnabled = x.enable); this.settingsService.getPlex().subscribe(x => this.plexEnabled = x.enable);
this.settingsService.getEmby().subscribe(x => this.embyEnabled = x.enable); this.settingsService.getEmby().subscribe(x => this.embyEnabled = x.enable);
<<<<<<< HEAD
this.settingsService.getJellyfin().subscribe(x => this.jellyfinEnabled = x.enable); this.settingsService.getJellyfin().subscribe(x => this.jellyfinEnabled = x.enable);
=======
this.settingsService.getLdap().subscribe(x => this.ldapEnabled = x.isEnabled);
>>>>>>> 691c70804f203fab858b1079a1cf3d5e4adbf322
} }
public submit(): void { public submit(): void {
@ -110,9 +122,14 @@ export class UserManagementComponent implements OnInit {
this.settings.defaultRoles = enabledClaims.map((claim) => claim.value); this.settings.defaultRoles = enabledClaims.map((claim) => claim.value);
this.settings.bannedPlexUserIds = this.bannedPlexUsers.map((u) => u.id); this.settings.bannedPlexUserIds = this.bannedPlexUsers.map((u) => u.id);
this.settings.bannedEmbyUserIds = this.bannedEmbyUsers.map((u) => u.id); this.settings.bannedEmbyUserIds = this.bannedEmbyUsers.map((u) => u.id);
<<<<<<< HEAD
this.settings.bannedJellyfinUserIds = this.bannedJellyfinUsers.map((u) => u.id); this.settings.bannedJellyfinUserIds = this.bannedJellyfinUsers.map((u) => u.id);
if(this.settings.importEmbyUsers || this.settings.importJellyfinUsers || this.settings.importPlexUsers) { if(this.settings.importEmbyUsers || this.settings.importJellyfinUsers || this.settings.importPlexUsers) {
=======
if(this.settings.importEmbyUsers || this.settings.importPlexUsers) {
>>>>>>> 691c70804f203fab858b1079a1cf3d5e4adbf322
this.enableImportButton = true; this.enableImportButton = true;
} }
@ -138,10 +155,14 @@ export class UserManagementComponent implements OnInit {
} }
public runImporter(): void { public runImporter(): void {
this.jobService.runPlexImporter().subscribe(); this.jobService.runPlexImporter().subscribe();
this.jobService.runEmbyImporter().subscribe(); this.jobService.runEmbyImporter().subscribe();
<<<<<<< HEAD
this.jobService.runJellyfinImporter().subscribe(); this.jobService.runJellyfinImporter().subscribe();
=======
this.jobService.runLdapImporter().subscribe();
>>>>>>> 691c70804f203fab858b1079a1cf3d5e4adbf322
} }
private filter(query: string, users: IUsersModel[]): IUsersModel[] { private filter(query: string, users: IUsersModel[]): IUsersModel[] {

View file

@ -31,12 +31,12 @@
<th mat-header-cell *matHeaderCellDef mat-sort-header> Username </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> Username </th>
<td mat-cell *matCellDef="let element"> {{element.userName}} </td> <td mat-cell *matCellDef="let element"> {{element.userName}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="alias"> <ng-container matColumnDef="alias">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Alias </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> Alias </th>
<td mat-cell *matCellDef="let element"> {{element.alias}} </td> <td mat-cell *matCellDef="let element"> {{element.alias}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="email"> <ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Email </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> Email </th>
<td mat-cell *matCellDef="let element"> {{element.emailAddress}} </td> <td mat-cell *matCellDef="let element"> {{element.emailAddress}} </td>
@ -44,7 +44,7 @@
<ng-container matColumnDef="remainingRequests"> <ng-container matColumnDef="remainingRequests">
<th mat-header-cell *matHeaderCellDef> Requests Remaining </th> <th mat-header-cell *matHeaderCellDef> Requests Remaining </th>
<td mat-cell *matCellDef="let u"> <td mat-cell *matCellDef="let u">
<div *ngIf="u.movieRequestQuota != null && u.movieRequestQuota.hasLimit"> <div *ngIf="u.movieRequestQuota != null && u.movieRequestQuota.hasLimit">
{{'UserManagment.MovieRemaining' | translate: {remaining: u.movieRequestQuota.remaining, total: u.movieRequestLimit} }} {{'UserManagment.MovieRemaining' | translate: {remaining: u.movieRequestQuota.remaining, total: u.movieRequestLimit} }}
</div> </div>
@ -59,7 +59,7 @@
<ng-container matColumnDef="nextRequestDue"> <ng-container matColumnDef="nextRequestDue">
<th mat-header-cell *matHeaderCellDef> Next Request Due </th> <th mat-header-cell *matHeaderCellDef> Next Request Due </th>
<td mat-cell *matCellDef="let u"> <td mat-cell *matCellDef="let u">
<div *ngIf="u.movieRequestQuota != null && u.movieRequestQuota.remaining != u.movieRequestLimit"> <div *ngIf="u.movieRequestQuota != null && u.movieRequestQuota.remaining != u.movieRequestLimit">
{{'UserManagment.MovieDue' | translate: {date: (u.movieRequestQuota.nextRequest | amLocal | amDateFormat: 'l LT')} }} {{'UserManagment.MovieDue' | translate: {date: (u.movieRequestQuota.nextRequest | amLocal | amDateFormat: 'l LT')} }}
</div> </div>
@ -90,6 +90,7 @@
<span *ngIf="u.userType === 3">Emby User</span> <span *ngIf="u.userType === 3">Emby User</span>
<span *ngIf="u.userType === 4">Emby Connect User</span> <span *ngIf="u.userType === 4">Emby Connect User</span>
<span *ngIf="u.userType === 5">Jellyfin User</span> <span *ngIf="u.userType === 5">Jellyfin User</span>
<span *ngIf="u.userType === 6">LDAP User</span> </td>
</td> </td>
</ng-container> </ng-container>
@ -136,6 +137,7 @@
</div> </div>
</div> </div>
<<<<<<< HEAD
<mat-form-field appearance="outline" class="full"> <mat-form-field appearance="outline" class="full">
<mat-label>Movie Request Limit</mat-label> <mat-label>Movie Request Limit</mat-label>
<input matInput id="movieRequestLimit" name="movieRequestLimit" [(ngModel)]="bulkMovieLimit"> <input matInput id="movieRequestLimit" name="movieRequestLimit" [(ngModel)]="bulkMovieLimit">
@ -159,6 +161,23 @@
<button type="button" mat-raised-button (click)="bulkUpdate()">Update Users</button> <button type="button" mat-raised-button (click)="bulkUpdate()">Update Users</button>
=======
<div class="form-group">
<label for="movieRequestLimit" class="control-label">Movie Request Limit</label>
<div>
<input type="text" [(ngModel)]="bulkMovieLimit" class="form-control form-small form-control-custom " id="movieRequestLimit" name="movieRequestLimit" value="{{bulkMovieLimit}}">
</div>
</div>
<div class="form-group">
<label for="episodeRequestLimit" class="control-label">Episode Request Limit</label>
<div>
<input type="text" [(ngModel)]="bulkEpisodeLimit" class="form-control form-small form-control-custom " id="episodeRequestLimit" name="episodeRequestLimit" value="{{bulkEpisodeLimit}}">
</div>
</div>
<button type="button" class="btn btn-success-outline" (click)="bulkUpdate()">Update Users</button>
>>>>>>> 691c70804f203fab858b1079a1cf3d5e4adbf322
</p-sidebar> </p-sidebar>
</div> </div>
</div> </div>

View file

@ -11,6 +11,7 @@ using Ombi.Schedule.Jobs.Ombi;
using Ombi.Schedule.Jobs.Plex; using Ombi.Schedule.Jobs.Plex;
using Ombi.Schedule.Jobs.Plex.Interfaces; using Ombi.Schedule.Jobs.Plex.Interfaces;
using Ombi.Schedule.Jobs.Radarr; using Ombi.Schedule.Jobs.Radarr;
using Ombi.Schedule.Jobs.Ldap;
using Quartz; using Quartz;
namespace Ombi.Controllers.V1 namespace Ombi.Controllers.V1
@ -115,6 +116,16 @@ namespace Ombi.Controllers.V1
return true; return true;
} }
/// Runs the LDAP User importer
/// </summary>
/// <returns></returns>
[HttpPost("ldapuserimporter")]
public async Task<bool> LdapUserImporter()
{
await OmbiQuartz.TriggerJob(nameof(ILdapUserImporter), "LDAP");
return true;
}
/// <summary> /// <summary>
/// Runs the Plex Content Cacher /// Runs the Plex Content Cacher
/// </summary> /// </summary>

View file

@ -467,6 +467,28 @@ namespace Ombi.Controllers.V1
return await Get<AuthenticationSettings>(); return await Get<AuthenticationSettings>();
} }
/// <summary>
/// Save the LDAP settings.
/// </summary>
/// <param name="settings">The settings.</param>
/// <returns></returns>
[HttpPost("ldap")]
public async Task<bool> LdapSettings([FromBody] LdapSettings settings)
{
return await Save(settings);
}
/// <summary>
/// Gets the LDAP Settings.
/// </summary>
/// <returns></returns>
[HttpGet("ldap")]
[AllowAnonymous]
public async Task<LdapSettings> LdapSettings()
{
return await Get<LdapSettings>();
}
/// <summary> /// <summary>
/// Save the Radarr settings. /// Save the Radarr settings.
/// </summary> /// </summary>

View file

@ -51,8 +51,7 @@ namespace Ombi.Controllers.V1
{ {
if (!model.UsePlexOAuth) if (!model.UsePlexOAuth)
{ {
var user = await _userManager.FindByNameAsync(model.Username); var user = await _userManager.FindUser(model.Username);
if (user == null) if (user == null)
{ {
// Could this be an email login? // Could this be an email login?
@ -67,7 +66,6 @@ namespace Ombi.Controllers.V1
user.EmailLogin = true; user.EmailLogin = true;
} }
// Verify Password // Verify Password
if (await _userManager.CheckPasswordAsync(user, model.Password)) if (await _userManager.CheckPasswordAsync(user, model.Password))
{ {
@ -187,17 +185,11 @@ namespace Ombi.Controllers.V1
var account = await _plexOAuthManager.GetAccount(accessToken); var account = await _plexOAuthManager.GetAccount(accessToken);
// Get the ombi user // Get the ombi user
var user = await _userManager.FindByNameAsync(account.user.username); var user = await _userManager.FindUser(account.user.username);
if (user == null) if (user == null)
{ {
// Could this be an email login? return new UnauthorizedResult();
user = await _userManager.FindByEmailAsync(account.user.email);
if (user == null)
{
return new UnauthorizedResult();
}
} }
return await CreateToken(true, user); return await CreateToken(true, user);
@ -228,17 +220,11 @@ namespace Ombi.Controllers.V1
[HttpPost("requirePassword")] [HttpPost("requirePassword")]
public async Task<bool> DoesUserRequireAPassword([FromBody] UserAuthModel model) public async Task<bool> DoesUserRequireAPassword([FromBody] UserAuthModel model)
{ {
var user = await _userManager.FindByNameAsync(model.Username); var user = await _userManager.FindUser(model.Username);
if (user == null) if (user == null)
{ {
// Could this be an email login? return true;
user = await _userManager.FindByEmailAsync(model.Username);
if (user == null)
{
return true;
}
} }
var requires = await _userManager.RequiresPassword(user); var requires = await _userManager.RequiresPassword(user);