WIP LDAP authentication and LDAP importer

This commit is contained in:
Dylan Praul 2020-10-10 16:35:50 -04:00
commit 691c70804f
25 changed files with 703 additions and 48 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
using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Novell.Directory.Ldap;
using Ombi.Api.Emby;
using Ombi.Api.Plex;
using Ombi.Api.Plex.Models;
@ -49,19 +51,52 @@ namespace Ombi.Core.Authentication
IPasswordHasher<OmbiUser> passwordHasher, IEnumerable<IUserValidator<OmbiUser>> userValidators,
IEnumerable<IPasswordValidator<OmbiUser>> passwordValidators, ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<OmbiUser>> logger, IPlexApi plexApi,
IEmbyApiFactory embyApi, ISettingsService<EmbySettings> embySettings, ISettingsService<AuthenticationSettings> auth)
IEmbyApiFactory embyApi, ISettingsService<EmbySettings> embySettings, ISettingsService<AuthenticationSettings> auth,
ILdapUserManager ldapUserManager, ISettingsService<UserManagementSettings> userManagementSettings)
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
{
_plexApi = plexApi;
_embyApi = embyApi;
_embySettings = embySettings;
_authSettings = auth;
_ldapUserManager = ldapUserManager;
_userManagementSettings = userManagementSettings;
}
private readonly IPlexApi _plexApi;
private readonly IEmbyApiFactory _embyApi;
private readonly ILdapUserManager _ldapUserManager;
private readonly ISettingsService<EmbySettings> _embySettings;
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)
{
@ -83,6 +118,10 @@ namespace Ombi.Core.Authentication
{
return await CheckEmbyPasswordAsync(user, password);
}
if (user.UserType == UserType.LdapUser)
{
return await CheckLdapPasswordAsync(user, password);
}
return false;
}
@ -185,5 +224,42 @@ namespace Ombi.Core.Authentication
}
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="3.1.8" />
<PackageReference Include="MusicBrainzAPI" Version="2.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="Novell.Directory.Ldap.NETStandard" Version="3.3.1" />
<PackageReference Include="System.Diagnostics.Process" Version="4.3.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Core" Version="1.1.0" />
</ItemGroup>

View file

@ -50,6 +50,7 @@ using Ombi.Schedule.Jobs.Emby;
using Ombi.Schedule.Jobs.Ombi;
using Ombi.Schedule.Jobs.Plex;
using Ombi.Schedule.Jobs.Sonarr;
using Ombi.Schedule.Jobs.Ldap;
using Ombi.Store.Repository.Requests;
using Ombi.Updater;
using Ombi.Api.Telegram;
@ -98,6 +99,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IMusicSender, MusicSender>();
services.AddTransient<IMassEmailSender, MassEmailSender>();
services.AddTransient<IPlexOAuthManager, PlexOAuthManager>();
services.AddTransient<ILdapUserManager, LdapUserManager>();
services.AddTransient<IVoteEngine, VoteEngine>();
services.AddTransient<IDemoMovieSearchEngine, DemoMovieSearchEngine>();
services.AddTransient<IDemoTvSearchEngine, DemoTvSearchEngine>();
@ -218,6 +220,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IOmbiAutomaticUpdater, OmbiAutomaticUpdater>();
services.AddTransient<IPlexUserImporter, PlexUserImporter>();
services.AddTransient<IEmbyUserImporter, EmbyUserImporter>();
services.AddTransient<ILdapUserImporter, LdapUserImporter>();
services.AddTransient<IWelcomeEmail, WelcomeEmail>();
services.AddTransient<ICouchPotatoSync, CouchPotatoSync>();
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

@ -7,6 +7,7 @@ using Ombi.Helpers;
using Ombi.Schedule.Jobs;
using Ombi.Schedule.Jobs.Couchpotato;
using Ombi.Schedule.Jobs.Emby;
using Ombi.Schedule.Jobs.Ldap;
using Ombi.Schedule.Jobs.Lidarr;
using Ombi.Schedule.Jobs.Ombi;
using Ombi.Schedule.Jobs.Plex;
@ -54,6 +55,7 @@ namespace Ombi.Schedule
await AddDvrApps(s);
await AddSystem(s);
await AddNotifications(s);
await AddLdap(s);
// Run Quartz
await OmbiQuartz.Start();
@ -102,5 +104,9 @@ namespace Ombi.Schedule
{
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

@ -7,6 +7,7 @@ namespace Ombi.Settings.Settings.Models
public bool ImportPlexAdmin { get; set; }
public bool ImportPlexUsers { get; set; }
public bool ImportEmbyUsers { get; set; }
public bool ImportLdapUsers { get; set; }
public int MovieRequestLimit { get; set; }
public int EpisodeRequestLimit { get; set; }
public List<string> DefaultRoles { get; set; } = new List<string>();

View file

@ -34,5 +34,6 @@ namespace Ombi.Store.Entities
PlexUser = 2,
EmbyUser = 3,
EmbyConnectUser = 4,
LdapUser = 5,
}
}

View file

@ -173,6 +173,21 @@ export interface IAuthenticationSettings extends ISettings {
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 {
enabled: boolean;
fontAwesomeIcon: string;
@ -184,6 +199,7 @@ export interface IUserManagementSettings extends ISettings {
importPlexUsers: boolean;
importPlexAdmin: boolean;
importEmbyUsers: boolean;
importLdapUsers: boolean;
defaultRoles: string[];
movieRequestLimit: number;
episodeRequestLimit: number;

View file

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

View file

@ -38,6 +38,7 @@ import {
IVoteSettings,
ITwilioSettings,
IWebhookNotificationSettings,
ILdapSettings,
} from "../interfaces";
import { ServiceHelpers } from "./service.helpers";
@ -124,6 +125,14 @@ export class SettingsService extends ServiceHelpers {
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
public getLandingPage(): Observable<ILandingPageSettings> {
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

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

View file

@ -7,6 +7,7 @@
<button mat-menu-item [routerLink]="['/Settings/Issues']">Issues</button>
<button mat-menu-item [routerLink]="['/Settings/UserManagement']">User Management</button>
<button mat-menu-item [routerLink]="['/Settings/Authentication']">Authentication</button>
<button mat-menu-item [routerLink]="['/Settings/Ldap']">Ldap</button>
<button mat-menu-item [routerLink]="['/Settings/Vote']">Vote</button>
<button mat-menu-item [routerLink]="['/Settings/TheMovieDb']">The Movie Database</button>
</mat-menu>

View file

@ -13,13 +13,13 @@
Import Plex Users</mat-checkbox>
</div>
</div>
<div class="form-group">
<mat-checkbox id="importAdmin" [(ngModel)]="settings.importPlexAdmin">Import Plex Admin</mat-checkbox>
</div>
<div *ngIf="plexUsers">
<p>Plex Users excluded from Import</p>
<p-autoComplete [(ngModel)]="bannedPlexUsers" [suggestions]="filteredPlexUsers" [multiple]="true" field="username" (completeMethod)="filterPlexList($event)"></p-autoComplete>
</div>
</div>
@ -27,14 +27,20 @@
<div class="form-group">
<mat-checkbox id="importEmbyUsers" [(ngModel)]="settings.importEmbyUsers">Import Emby Users</mat-checkbox>
</div>
<div *ngIf="embyUsers">
<p>Emby Users excluded from Import</p>
<p-autoComplete [(ngModel)]="bannedEmbyUsers" [suggestions]="filteredEmbyUsers" [multiple]="true" field="username" (completeMethod)="filterEmbyList($event)"></p-autoComplete>
</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 class="col-md-6">
<h4>Default Roles</h4>
@ -71,7 +77,7 @@
</div>
<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>
</div>
</div>
<div class="md-form-field" style="margin-top:1em;"></div>
</fieldset>
</div>

View file

@ -12,6 +12,7 @@ export class UserManagementComponent implements OnInit {
public plexEnabled: boolean;
public embyEnabled: boolean;
public ldapEnabled: boolean;
public settings: IUserManagementSettings;
public claims: ICheckbox[];
@ -37,7 +38,7 @@ export class UserManagementComponent implements OnInit {
this.settingsService.getUserManagementSettings().subscribe(x => {
this.settings = x;
if(x.importEmbyUsers || x.importPlexUsers) {
if(x.importEmbyUsers || x.importPlexUsers || x.importLdapUsers) {
this.enableImportButton = true;
}
@ -80,6 +81,7 @@ export class UserManagementComponent implements OnInit {
});
this.settingsService.getPlex().subscribe(x => this.plexEnabled = x.enable);
this.settingsService.getEmby().subscribe(x => this.embyEnabled = x.enable);
this.settingsService.getLdap().subscribe(x => this.ldapEnabled = x.isEnabled);
}
public submit(): void {
@ -89,7 +91,7 @@ export class UserManagementComponent implements OnInit {
this.settings.defaultRoles = enabledClaims.map((claim) => claim.value);
this.settings.bannedPlexUserIds = this.bannedPlexUsers.map((u) => u.id);
this.settings.bannedEmbyUserIds = this.bannedEmbyUsers.map((u) => u.id);
if(this.settings.importEmbyUsers || this.settings.importPlexUsers) {
this.enableImportButton = true;
}
@ -112,9 +114,10 @@ export class UserManagementComponent implements OnInit {
}
public runImporter(): void {
this.jobService.runPlexImporter().subscribe();
this.jobService.runEmbyImporter().subscribe();
this.jobService.runLdapImporter().subscribe();
}
private filter(query: string, users: IUsersModel[]): IUsersModel[] {

View file

@ -31,12 +31,12 @@
<th mat-header-cell *matHeaderCellDef mat-sort-header> Username </th>
<td mat-cell *matCellDef="let element"> {{element.userName}} </td>
</ng-container>
<ng-container matColumnDef="alias">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Alias </th>
<td mat-cell *matCellDef="let element"> {{element.alias}} </td>
</ng-container>
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Email </th>
<td mat-cell *matCellDef="let element"> {{element.emailAddress}} </td>
@ -44,7 +44,7 @@
<ng-container matColumnDef="remainingRequests">
<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">
{{'UserManagment.MovieRemaining' | translate: {remaining: u.movieRequestQuota.remaining, total: u.movieRequestLimit} }}
</div>
@ -59,7 +59,7 @@
<ng-container matColumnDef="nextRequestDue">
<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">
{{'UserManagment.MovieDue' | translate: {date: (u.movieRequestQuota.nextRequest | amLocal | amDateFormat: 'l LT')} }}
</div>
@ -73,10 +73,10 @@
</ng-container>
<ng-container matColumnDef="lastLoggedIn">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Last Logged In </th>
<td mat-cell *matCellDef="let u">
<td mat-cell *matCellDef="let u">
<span *ngIf="u.lastLoggedIn">
{{u.lastLoggedIn | amLocal | amDateFormat: 'l LT'}}
</span>
</span>
<span *ngIf="!u.lastLoggedIn">
Not logged in yet!
</span> </td>
@ -87,12 +87,13 @@
<td mat-cell *matCellDef="let u">
<span *ngIf="u.userType === 1">Local User</span>
<span *ngIf="u.userType === 2">Plex User</span>
<span *ngIf="u.userType === 3">Emby User</span> </td>
<span *ngIf="u.userType === 3">Emby User</span>
<span *ngIf="u.userType === 5">LDAP User</span> </td>
</ng-container>
<ng-container matColumnDef="roles">
<th mat-header-cell *matHeaderCellDef> Roles </th>
<td mat-cell *matCellDef="let element">
<td mat-cell *matCellDef="let element">
<div *ngFor="let claim of element.claims">
<span *ngIf="claim.enabled">{{claim.value}}</span>
</div>
@ -112,7 +113,7 @@
<button *ngIf="!u.hasLoggedIn" mat-raised-button color="accent" (click)="welcomeEmail(u)" [disabled]="!customizationSettings?.applicationUrl">Send Welcome Email</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
@ -146,7 +147,7 @@
<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>
</p-sidebar>
</div>

View file

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

View file

@ -426,6 +426,28 @@ namespace Ombi.Controllers.V1
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>
/// Save the Radarr settings.
/// </summary>

View file

@ -48,22 +48,12 @@ namespace Ombi.Controllers.V1
{
if (!model.UsePlexOAuth)
{
var user = await _userManager.FindByNameAsync(model.Username);
var user = await _userManager.FindUser(model.Username);
if (user == null)
{
// Could this be an email login?
user = await _userManager.FindByEmailAsync(model.Username);
if (user == null)
{
return new UnauthorizedResult();
}
user.EmailLogin = true;
return new UnauthorizedResult();
}
// Verify Password
if (await _userManager.CheckPasswordAsync(user, model.Password))
{
@ -184,17 +174,11 @@ namespace Ombi.Controllers.V1
var account = await _plexOAuthManager.GetAccount(accessToken);
// Get the ombi user
var user = await _userManager.FindByNameAsync(account.user.username);
var user = await _userManager.FindUser(account.user.username);
if (user == null)
{
// Could this be an email login?
user = await _userManager.FindByEmailAsync(account.user.email);
if (user == null)
{
return new UnauthorizedResult();
}
return new UnauthorizedResult();
}
return await CreateToken(true, user);
@ -225,17 +209,11 @@ namespace Ombi.Controllers.V1
[HttpPost("requirePassword")]
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)
{
// Could this be an email login?
user = await _userManager.FindByEmailAsync(model.Username);
if (user == null)
{
return true;
}
return true;
}
var requires = await _userManager.RequiresPassword(user);