Skip to content

Commit

Permalink
#10 Заменил ActiveDirectory на работу с контейнером OpenLDAP
Browse files Browse the repository at this point in the history
  • Loading branch information
GregoryGhost committed Apr 28, 2020
1 parent e4a59e7 commit 85f28ce
Show file tree
Hide file tree
Showing 13 changed files with 174 additions and 50 deletions.
2 changes: 1 addition & 1 deletion Idone/Idone.Back/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public void ConfigureServices(IServiceCollection services)
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2).Services
.AddIdoneIdentity()
.AddIdoneDb(connString)
.AddSecurityDi("TODO:none");
.AddSecurityDi("TODO:none", "cn=admin,dc=example,dc=org", "admin");
}
}
}
10 changes: 5 additions & 5 deletions Idone/Idone.DAL/DTO/DtoAdUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@ public class DtoAdUser : Record<DtoAdUser>
/// <summary>
/// Контруктор по умолчанию.
/// </summary>
/// <param name="sid"> SID пользователя. </param>
/// <param name="uid"> Составной идентификатор пользователя.. </param>
/// <param name="surname"> Фамилия пользователя. </param>
/// <param name="name"> Имя пользователя. </param>
/// <param name="patronomic"> Отчество пользователя. </param>
/// <param name="email"> Почта пользователя. </param>
public DtoAdUser(
string sid,
string uid,
string surname,
string name,
string patronomic,
string email)
{
Sid = sid;
Uid = uid;
Surname = surname;
Name = name;
Patronomic = patronomic;
Expand All @@ -45,9 +45,9 @@ public class DtoAdUser : Record<DtoAdUser>
public string Patronomic { get; }

/// <summary>
/// Идентификатор безопасности пользователя.
/// Составной идентификатор пользователя.
/// </summary>
public string Sid { get; }
public string Uid { get; }

/// <summary>
/// Фамилия.
Expand Down
1 change: 0 additions & 1 deletion Idone/Idone.Security/EnterPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
{
using System;
using System.Collections.Generic;
using System.DirectoryServices.AccountManagement;
using System.Linq;

using Idone.DAL.Dictionaries;
Expand Down
8 changes: 4 additions & 4 deletions Idone/Idone.Security/Idone.Security.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
<TargetFramework>netcoreapp2.2</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="4.5.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Idone.Core\Idone.Core.csproj" />
<ProjectReference Include="..\Idone.DAL\Idone.DAL.csproj" />
Expand All @@ -28,4 +24,8 @@
</Reference>
</ItemGroup>

<ItemGroup>
<PackageReference Include="LdapForNet" Version="2.3.0" />
</ItemGroup>

</Project>
7 changes: 4 additions & 3 deletions Idone/Idone.Security/SecurityApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
{
using Idone.Security.Services;

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

/// <summary>
Expand All @@ -15,11 +14,13 @@ public static class SecurityApp
/// </summary>
/// <param name="services"> Сервисы. </param>
/// <param name="adDomain"> Домен Active Directory сервиса. </param>
/// <param name="adLogin">Логин в LDAP аля "cn=admin,dc=example,dc=org".</param>
/// <returns> Возвращает сервисы. </returns>
public static IServiceCollection AddSecurityDi(this IServiceCollection services, string adDomain)
public static IServiceCollection AddSecurityDi(this IServiceCollection services, string adDomain, string adLogin,
string adPswd)
{
services.AddScoped<UserService>();
services.AddScoped(s => new AdService(adDomain));
services.AddScoped(s => new AdService(adDomain, adLogin, adPswd));

return services;
}
Expand Down
119 changes: 86 additions & 33 deletions Idone/Idone.Security/Services/AdService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ namespace Idone.Security.Services
{
using System;
using System.Collections.Generic;
using System.DirectoryServices.AccountManagement;
using System.Linq;
using System.Net;

Expand All @@ -11,37 +10,60 @@ namespace Idone.Security.Services

using LanguageExt;

using LdapForNet;
using LdapForNet.Native;

using static LanguageExt.Prelude;

using Success = Idone.DAL.Dictionaries.Success;

/// <summary>
/// Сервис по работе с AD-пользователями.
/// </summary>
internal class AdService
public partial class AdService
{
/// <summary>
/// Домен сервиса Active Directory.
/// </summary>
private readonly string _domain;

private readonly string _domainComponent;

private readonly string _login;

private readonly string _pswd;

private readonly string _uidAdmin;

/// <summary>
/// Инициализировать зависимости.
/// </summary>
/// <param name="domain">Домен сервиса Active Directory.</param>
public AdService(string domain)
/// <param name="login">Логин в LDAP аля "cn=admin,dc=example,dc=org".</param>
/// <param name="pswd">Пароль пользователя.</param>
/// <param name="domainComponent">Компонент Домена.</param>
public AdService(string domain, string login, string pswd, string adminNickname = "admin",
string domainComponent = "dc=example,dc=org")
{
if (string.IsNullOrEmpty(domain))
{
throw new NullReferenceException($"Пустой аргумент {nameof(domain)}");
}

if (!Dns.GetHostAddresses(domain).Any())
{
var msg = $"Не найден домен сервиса Active Directory для переданного аргумента {nameof(domain)}";
throw new ArgumentException(msg);
}

_domain = domain;
_login = login;
_pswd = pswd;
_uidAdmin = FormatUserUid(adminNickname, domainComponent);
_domainComponent = domainComponent;
}

private static string FormatUserUid(string nickname, string domainCompany)
{
return $"cn={nickname},{domainCompany}";
}

/// <summary>
Expand All @@ -51,48 +73,79 @@ public AdService(string domain)
/// <returns> Возращает монаду с найденными совпадениями пользователей по отображаемому имени. </returns>
public Either<Error, IEnumerable<DtoAdUser>> FindUsersByDisplayName(string searchExpression)
{
using (var ctx = new PrincipalContext(ContextType.Domain, _domain))
using (var query = new UserPrincipal(ctx)
{
DisplayName = searchExpression
})
using (var cn = new LdapConnection())
{
var foundUsers = new PrincipalSearcher(query).FindAll().Where(user => user.DisplayName != null).Select(
userData =>
{
var user = userData as UserPrincipal;
return new DtoAdUser(
user?.Sid.Value,
user?.Surname,
user?.GivenName,
user?.MiddleName ?? string.Empty,
user?.EmailAddress);
});
cn.Connect(_domain);
cn.Bind(Native.LdapAuthMechanism.SIMPLE, _login, _pswd);
var adUsers = cn.Search(
_domainComponent,
$"(displayName={searchExpression})");

var foundUsers =
adUsers.Select(user => ToAdUser(user, _domainComponent)).
Rights(); //TODO: писать в лог Left вариант Either'а через Where

return Right<Error, IEnumerable<DtoAdUser>>(foundUsers);
}
}

private static Either<DictValueCases, DtoAdUser> ToAdUser(LdapEntry userData,
string domainCompany)
{
var nicknameValue = GetValue(userData.Attributes, "uid");
var surnameValue = GetValue(userData.Attributes, "sn");
var givenNameValue = GetValue(userData.Attributes, "givenName");
var emailValue = GetValue(userData.Attributes, "mail");
var middleNameValue = GetValue(userData.Attributes, "middleName");

var adUser = from nickname in nicknameValue
from surname in surnameValue
from givenName in givenNameValue
from email in emailValue
let uid = FormatUserUid(nickname, domainCompany)
select new DtoAdUser(uid, surname, givenName, middleNameValue.IfLeft(string.Empty), email);

return adUser;
}

private static Either<DictValueCases, string> GetValue(IReadOnlyDictionary<string, List<string>> attributes, string key)
{
var leftCase = lpar<DictValueCase, string, DictValueCases>(DictValueCases.Create, key);
if (attributes.TryGetValue(key, out var values))
return values.Any()
? Right(values.First())
: Left<DictValueCases, string>(leftCase(DictValueCase.EmptyValue));

return Left(leftCase(DictValueCase.WrongKey));
}

/// <summary>
/// Создать AD-пользователя.
/// </summary>
/// <param name="newUser">Данные нового пользователя.</param>
/// <returns>Результат операции.</returns>
public Either<Error, Success> CreateUser(DtoNewAdUser newUser)
{
using (var ctx = new PrincipalContext(ContextType.Domain, _domain))
using (var query = new UserPrincipal(ctx))
using (var cn = new LdapConnection())
{
query.SamAccountName = newUser.Nickname;
query.EmailAddress = newUser.Email;
query.SetPassword(newUser.Password);
query.DisplayName = $"{newUser.Surname} {newUser.Name} {newUser.Patronomic}";
query.GivenName = newUser.Name;
query.Surname = newUser.Surname;
query.MiddleName = newUser.Patronomic;
query.Enabled = true;
query.ExpirePasswordNow();
query.Save();
cn.Connect(_domain);
cn.Bind(Native.LdapAuthType.Simple, new LdapCredential { UserName = _login, Password = _pswd });
cn.Add(
new LdapEntry
{
Dn = FormatUserUid(newUser.Nickname, _domainComponent), //TODO: вынести в метод формирования=
Attributes = new Dictionary<string, List<string>>
{
{ "sn", new List<string> { newUser.Surname } },
{ "objectclass", new List<string> { "inetOrgPerson" } },
{ "givenName", new List<string> { newUser.Name } },
{ "displayName", new List<string> { $"{newUser.Surname} {newUser.Name} {newUser.Patronomic}" } },
{ "title", new List<string> { newUser.Patronomic } },
{ "mail", new List<string> { newUser.Email } },
{ "uid", new List<string> { newUser.Nickname } },
{ "userPassword", new List<string> { newUser.Password } }
}
});
}

return Right<Error, Success>(Success.ItsSuccess);
Expand Down
27 changes: 27 additions & 0 deletions Idone/Idone.Security/Services/DictValueCase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Idone.Security.Services
{
public partial class AdService
{
internal enum DictValueCase
{
WrongKey = 0,

EmptyValue = 1
}

internal class DictValueCases
{
private DictValueCases(DictValueCase valueCase, string value)
{
DictValueCase = (valueCase, value);
}

public (DictValueCase, string) DictValueCase { get; }

public static DictValueCases Create(DictValueCase valueCase, string value)
{
return new DictValueCases(valueCase, value);
}
}
}
}
3 changes: 3 additions & 0 deletions Idone/Idone.Tests/Constants.fs
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,7 @@ module Constants =
Port = "1434"
Env = DOCKER_AD_ENV
}

let AD_LOGIN = "cn=admin,dc=example,dc=org"
let AD_PASSWORD = "admin"

2 changes: 1 addition & 1 deletion Idone/Idone.Tests/Helpers/IdoneApiHelper.fs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module IdoneApiHelper =

let fillUserCredentials (userData : DtoAdUser) : DtoRegistrateUser =
new DtoRegistrateUser(
userData.Sid,
userData.Uid,
userData.Surname,
userData.Name,
userData.Patronomic,
Expand Down
1 change: 1 addition & 0 deletions Idone/Idone.Tests/Idone.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Compile Include="Sample.fs" />
<Compile Include="SampleLdap.fs" />
<Compile Include="Main.fs" />
</ItemGroup>

Expand Down
2 changes: 1 addition & 1 deletion Idone/Idone.Tests/Main.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ module EntryPoint =

[<EntryPoint>]
let main argv =
Tests.runTestsInAssembly defaultConfig argv
Tests.runTestsInAssembly defaultConfig argv
2 changes: 1 addition & 1 deletion Idone/Idone.Tests/Sample.fs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ module Tests =
let services = new ServiceCollection()
services.AddIdoneIdentity()
.AddIdoneDb(connString)
.AddSecurityDi(domain) |> ignore
.AddSecurityDi(domain, AD_LOGIN, AD_PASSWORD) |> ignore
let rootServiceProvider = services.BuildServiceProvider()
use scope = rootServiceProvider.CreateScope()
scope.ServiceProvider.GetRequiredService<AppContext>().InitTest()
Expand Down
40 changes: 40 additions & 0 deletions Idone/Idone.Tests/SampleLdap.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
namespace Idone.Tests

module AdTests =
open Expecto

open LanguageExt.UnsafeValueAccess

open Idone.Security.Services
open Idone.DAL.DTO

open Idone.Tests.Extensions
open Idone.Tests.Constants

open Newtonsoft.Json

let tests =
let domain = "172.17.0.4"
testSequencedGroup "Последовательное выполнение тестов по работе с OpenLDAP"
<| testList "Проверка работы с АД" [
// test "Создание пользователя" {
// let ad = new AdService(domain, AD_LOGIN, AD_PASSWORD)
// let newUser = new DtoNewAdUser("Кулаков", "Григорий", "Викторович", "test@mail.ru", "gregory", "qweQWE1234")
// let createdUser = ad.CreateUser newUser
//
// Expect.isRight createdUser
// }
test "Получения пользователя" {
let ad = new AdService(domain, AD_LOGIN, AD_PASSWORD)

let filterDisplayName = "Кулаков*"
let foundUsers = ad.FindUsersByDisplayName filterDisplayName

printfn "found users %s" <| JsonConvert.SerializeObject(foundUsers)

Expect.isRight foundUsers
if foundUsers.ValueUnsafe() |> Seq.isEmpty then
let errorMsg = sprintf "Not found AD-user by filter %s" filterDisplayName
raise <| new System.Exception(errorMsg)
}
]

0 comments on commit 85f28ce

Please sign in to comment.