You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Ombi/src/Ombi.Core/Authentication/OmbiUserManager.cs

247 lines
10 KiB

#region Copyright
// /************************************************************************
// Copyright (c) 2017 Jamie Rees
// File: OmbiUserManager.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Ombi.Api.Emby;
using Ombi.Api.Jellyfin;
using Ombi.Api.Plex;
using Ombi.Api.Plex.Models;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
namespace Ombi.Core.Authentication
{
public class OmbiUserManager : UserManager<OmbiUser>
{
public OmbiUserManager(IUserStore<OmbiUser> store, IOptions<IdentityOptions> optionsAccessor,
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,
IJellyfinApiFactory jellyfinApi, ISettingsService<JellyfinSettings> jellyfinSettings,
ISettingsService<AuthenticationSettings> auth)
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
{
_plexApi = plexApi;
_embyApi = embyApi;
_jellyfinApi = jellyfinApi;
_embySettings = embySettings;
_jellyfinSettings = jellyfinSettings;
_authSettings = auth;
}
private readonly IPlexApi _plexApi;
private readonly IEmbyApiFactory _embyApi;
private readonly IJellyfinApiFactory _jellyfinApi;
private readonly ISettingsService<EmbySettings> _embySettings;
private readonly ISettingsService<JellyfinSettings> _jellyfinSettings;
private readonly ISettingsService<AuthenticationSettings> _authSettings;
private string _clientIpAddress;
public string ClientIpAddress { get => _clientIpAddress; set => _clientIpAddress = value; }
public override async Task<bool> CheckPasswordAsync(OmbiUser user, string password)
{
var requiresPassword = await RequiresPassword(user);
if (!requiresPassword)
{
// Let them through!
return true;
}
if (user.UserType == UserType.LocalUser)
{
return await base.CheckPasswordAsync(user, password);
}
if (user.UserType == UserType.PlexUser)
{
return await CheckPlexPasswordAsync(user, password);
}
if (user.UserType == UserType.EmbyUser || user.UserType == UserType.EmbyConnectUser)
{
return await CheckEmbyPasswordAsync(user, password, ClientIpAddress);
}
if (user.UserType == UserType.JellyfinUser)
{
return await CheckJellyfinPasswordAsync(user, password);
}
return false;
}
public async Task<bool> RequiresPassword(OmbiUser user)
{
var authSettings = await _authSettings.GetSettingsAsync();
if (authSettings.AllowNoPassword)
{
var roles = await GetRolesAsync(user);
if (roles.Contains(OmbiRoles.Admin) || roles.Contains(OmbiRoles.PowerUser))
{
// We require a password
return true;
}
return false;
}
return true;
}
public async Task<OmbiUser> GetOmbiUserFromPlexToken(string plexToken)
{
var plexAccount = await _plexApi.GetAccount(plexToken);
// Check for a ombi user
if (plexAccount?.user == null)
{
return null;
}
var potentialOmbiUser = await Users.FirstOrDefaultAsync(x =>
x.ProviderUserId == plexAccount.user.id);
// Update ombi user with the token
if (potentialOmbiUser != null)
{
potentialOmbiUser.MediaServerToken = plexAccount.user.authentication_token;
await UpdateAsync(potentialOmbiUser);
}
return potentialOmbiUser;
}
/// <summary>
/// Sign the user into plex and make sure we can get the authentication token.
/// <remarks>We do not check if the user is in the owners "friends" since they must have a local user account to get this far</remarks>
/// </summary>
/// <param name="user"></param>
/// <param name="password"></param>
/// <returns></returns>
private async Task<bool> CheckPlexPasswordAsync(OmbiUser user, string password)
{
var login = user.EmailLogin ? user.Email : user.UserName;
var result = await _plexApi.SignIn(new UserRequest { password = password, login = login });
if (result.user?.authentication_token != null)
{
// Update ombi user with the token
user.MediaServerToken = result.user?.authentication_token;
await UpdateAsync(user);
return true;
}
return false;
}
/// <summary>
/// Sign the user into Emby
/// <remarks>We do not check if the user is in the owners "friends" since they must have a local user account to get this far.
/// We also have to try and authenticate them with every server, the first server that work we just say it was a success</remarks>
/// </summary>
/// <param name="user"></param>
/// <param name="password"></param>
/// <returns></returns>
private async Task<bool> CheckEmbyPasswordAsync(OmbiUser user, string password, string clientIpAddress="")
{
var embySettings = await _embySettings.GetSettingsAsync();
var client = _embyApi.CreateClient(embySettings);
if (user.IsEmbyConnect)
{
var result = await client.LoginConnectUser(user.UserName, password);
if (result.AccessToken.HasValue())
{
// We cannot update the email address in the user importer due to there is no way
// To get this info from Emby Connect without the username and password.
// So we do it here!
var email = user.Email ?? string.Empty;
if (!email.Equals(result.User?.Email))
{
user.Email = result.User?.Email;
await UpdateAsync(user);
}
return true;
}
}
foreach (var server in embySettings.Servers)
{
try
{
var result = await client.LogIn(user.UserName, password, server.ApiKey, server.FullUri, clientIpAddress);
if (result != null)
{
return true;
}
}
catch (Exception e)
{
Logger.LogError(e, "Emby Login Failed");
}
}
return false;
}
/// <summary>
/// Sign the user into Jellyfin
/// <remarks>We do not check if the user is in the owners "friends" since they must have a local user account to get this far.
/// We also have to try and authenticate them with every server, the first server that work we just say it was a success</remarks>
/// </summary>
/// <param name="user"></param>
/// <param name="password"></param>
/// <returns></returns>
private async Task<bool> CheckJellyfinPasswordAsync(OmbiUser user, string password)
{
var jellyfinSettings = await _jellyfinSettings.GetSettingsAsync();
var client = _jellyfinApi.CreateClient(jellyfinSettings);
foreach (var server in jellyfinSettings.Servers)
{
try
{
var result = await client.LogIn(user.UserName, password, server.ApiKey, server.FullUri);
if (result != null)
{
return true;
}
}
catch (Exception e)
{
Logger.LogError(e, "Jellyfin Login Failed");
}
}
return false;
}
}
}