Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions src/Server/LdapProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Serilog;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
Expand Down Expand Up @@ -230,15 +231,14 @@ private async Task DataExchange(TcpClient source, Stream sourceStream, TcpClient

if (_clientConfig.CheckUserGroups())
{
profile.MemberOf = await _ldapService.GetAllGroups(_serverStream, profile, _clientConfig);

//check ACL
if (_clientConfig.ActiveDirectoryGroup.Any())
{
var accessGroup = _clientConfig.ActiveDirectoryGroup.FirstOrDefault(group => IsMemberOf(profile, group));
if (accessGroup != null)
var memberOfResult = await IsMemberOfAny(profile, _clientConfig.ActiveDirectoryGroup);

if (memberOfResult.IsMember)
{
_logger.Debug($"User '{{user:l}}' is member of '{accessGroup.Trim()}' access group in {profile.BaseDn}", _userName);
_logger.Debug($"User '{{user:l}}' is member of '{memberOfResult.Group.Trim()}' access group in {profile.BaseDn}", _userName);
}
else
{
Expand All @@ -259,10 +259,10 @@ private async Task DataExchange(TcpClient source, Stream sourceStream, TcpClient
//check if mfa is mandatory
if (_clientConfig.ActiveDirectory2FaGroup.Any())
{
var mfaGroup = _clientConfig.ActiveDirectory2FaGroup.FirstOrDefault(group => IsMemberOf(profile, group));
if (mfaGroup != null)
var memberOfResult = await IsMemberOfAny(profile, _clientConfig.ActiveDirectory2FaGroup);
if (memberOfResult.IsMember)
{
_logger.Debug($"User '{{user:l}}' is member of '{mfaGroup.Trim()}' 2FA group in {profile.BaseDn}", _userName);
_logger.Debug($"User '{{user:l}}' is member of '{memberOfResult.Group.Trim()}' 2FA group in {profile.BaseDn}", _userName);
}
else
{
Expand All @@ -274,10 +274,10 @@ private async Task DataExchange(TcpClient source, Stream sourceStream, TcpClient
//check of mfa is not mandatory
if (_clientConfig.ActiveDirectory2FaBypassGroup.Any() && !bypass)
{
var bypassGroup = _clientConfig.ActiveDirectory2FaBypassGroup.FirstOrDefault(group => IsMemberOf(profile, group));
if (bypassGroup != null)
var memberOfResult = await IsMemberOfAny(profile, _clientConfig.ActiveDirectory2FaBypassGroup);
if (memberOfResult.IsMember)
{
_logger.Information($"User '{{user:l}}' is member of '{bypassGroup.Trim()}' 2FA bypass group in {profile.BaseDn}", _userName);
_logger.Information($"User '{{user:l}}' is member of '{memberOfResult.Group.Trim()}' 2FA bypass group in {profile.BaseDn}", _userName);
bypass = true;
}
else
Expand Down Expand Up @@ -453,9 +453,9 @@ private bool IsServiceAccount(string userName)
return false;
}

private bool IsMemberOf(LdapProfile profile, string group)
private async Task<MemberOfResult> IsMemberOfAny(LdapProfile profile, IEnumerable<string> groups)
{
return profile.MemberOf?.Any(g => g.ToLower() == group.ToLower().Trim()) ?? false;
return await _ldapService.IsMemberOf(_serverStream, profile, _clientConfig, groups);
}
}

Expand Down
30 changes: 29 additions & 1 deletion src/Services/LdapService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using MultiFactor.Ldap.Adapter.Services.MemberOf;

namespace MultiFactor.Ldap.Adapter.Services
{
Expand Down Expand Up @@ -367,7 +368,7 @@ public async Task<LdapProfile> LoadProfile(Stream ldapConnectedStream, string us
mailEntries.Add(entry);
break;
case "memberOf":
profile.MemberOf.AddRange(entry.Values.Select(v => DnToCn(v)));
profile.MemberOf.AddRange(entry.Values);
break;
}
}
Expand Down Expand Up @@ -408,6 +409,29 @@ public async Task<List<string>> GetAllGroups(Stream ldapConnectedStream, LdapPro
return groups;
}

public async Task<MemberOfResult> IsMemberOf(Stream ldapConnectedStream, LdapProfile profile, ClientConfiguration clientConfiguration, IEnumerable<string> groupDns)
{
if (!clientConfiguration.LoadActiveDirectoryNestedGroups)
{
var intersections = profile.MemberOf?.Select(FormatDn).Intersect(groupDns.Select(FormatDn))?.ToList() ?? new List<string>();
var isMember = intersections.Any();
var group = intersections.FirstOrDefault();
return new MemberOfResult(isMember, group);
}

IMemberOfService memberShipService = string.IsNullOrWhiteSpace(clientConfiguration.LdapBaseDn) ? new ActiveDirectoryMemberOfService() : new FreeIpaMemberOfService();
foreach (var group in groupDns)
{
var isMemberOf = await memberShipService.IsMemberOf(ldapConnectedStream, profile, group, _messageId++);
if (!isMemberOf)
continue;

return new MemberOfResult(true, group);
}

return new MemberOfResult(false, string.Empty);
}

#endregion

private IEnumerable<string> GetGroups(LdapPacket packet)
Expand Down Expand Up @@ -546,5 +570,9 @@ private class LdapSearchResultEntry
public string Name { get; set; }
public IList<string> Values { get; set; }
}

public static string FormatDn(string dn) => dn.ToLower().Replace(" ", string.Empty);
}

public record MemberOfResult(bool IsMember, string Group);
}
86 changes: 86 additions & 0 deletions src/Services/MemberOf/ActiveDirectoryMemberOfService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using MultiFactor.Ldap.Adapter.Core;

namespace MultiFactor.Ldap.Adapter.Services.MemberOf;

public class ActiveDirectoryMemberOfService : IMemberOfService
{
public async Task<bool> IsMemberOf(Stream ldapConnectedStream, LdapProfile profile, string groupDn, int messageId)
{
var filter = GetFilter(groupDn);
var request = BuildMemberOfRequest(profile.Dn, filter, messageId);
var requestData = request.GetBytes();
await ldapConnectedStream.WriteAsync(requestData, 0, requestData.Length);
var users = new List<string>();
LdapPacket packet;
while ((packet = await LdapPacket.ParsePacket(ldapConnectedStream)) != null)
{
users.AddRange(GetSearchResult(packet));
}

return users.Any();
}

private LdapAttribute[] GetFilter(string groupDn)
{
return new[]
{
new LdapAttribute((byte)LdapFilterChoice.extensibleMatch)
{
ChildAttributes =
{
new LdapAttribute(1, "1.2.840.113556.1.4.1941"),
new LdapAttribute(2, "memberof"),
new LdapAttribute(3, groupDn),
new LdapAttribute(4, (byte)0)
}
}
};
}

private LdapPacket BuildMemberOfRequest(string userName, LdapAttribute[] memberFilter, int messageId)
{
var packet = new LdapPacket(messageId);

var searchRequest = new LdapAttribute(LdapOperation.SearchRequest);
searchRequest.ChildAttributes.Add(new LdapAttribute(UniversalDataType.OctetString, userName)); //base dn
searchRequest.ChildAttributes.Add(new LdapAttribute(UniversalDataType.Enumerated, (byte)2)); //scope: subtree
searchRequest.ChildAttributes.Add(new LdapAttribute(UniversalDataType.Enumerated, (byte)0)); //aliases: never
searchRequest.ChildAttributes.Add(new LdapAttribute(UniversalDataType.Integer, (byte)0)); //size limit: unset
searchRequest.ChildAttributes.Add(new LdapAttribute(UniversalDataType.Integer, (byte)60)); //time limit: 60
searchRequest.ChildAttributes.Add(new LdapAttribute(UniversalDataType.Boolean, true)); //typesOnly: true

foreach (var attribute in memberFilter)
{
searchRequest.ChildAttributes.Add(attribute);
}

packet.ChildAttributes.Add(searchRequest);

var attrList = new LdapAttribute(UniversalDataType.Sequence);
attrList.ChildAttributes.Add(new LdapAttribute(UniversalDataType.OctetString, "distinguishedName"));

searchRequest.ChildAttributes.Add(attrList);

return packet;
}

private IEnumerable<string> GetSearchResult(LdapPacket packet)
{
var searchResults = new List<string>();

foreach (var searchResultEntry in packet.ChildAttributes.FindAll(attr => attr.LdapOperation == LdapOperation.SearchResultEntry))
{
if (searchResultEntry.ChildAttributes.Count > 0)
{
var result = searchResultEntry.ChildAttributes[0].GetValue<string>();
searchResults.Add(result);
}
}

return searchResults;
}
}
97 changes: 97 additions & 0 deletions src/Services/MemberOf/FreeIpaMemberOfService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using MultiFactor.Ldap.Adapter.Core;

namespace MultiFactor.Ldap.Adapter.Services.MemberOf;

public class FreeIpaMemberOfService : IMemberOfService
{
private List<string> _groups = new List<string>();

public async Task<bool> IsMemberOf(Stream ldapConnectedStream, LdapProfile profile, string groupDn, int messageId)
{
var groups = await GetUserGroups(ldapConnectedStream, profile, messageId);
return groups.Any(g => g == LdapService.FormatDn(groupDn));
}

private async Task<List<string>> GetUserGroups(Stream ldapConnectedStream, LdapProfile profile, int messageId)
{
if (_groups.Count > 0)
return _groups;

var filter = GetFilter(profile.Dn);
var request = BuildMemberOfRequest(profile.Dn, filter, messageId);
var requestData = request.GetBytes();
await ldapConnectedStream.WriteAsync(requestData, 0, requestData.Length);
LdapPacket packet;
while ((packet = await LdapPacket.ParsePacket(ldapConnectedStream)) != null)
{
_groups.AddRange(GetSearchResult(packet));
}

return _groups;
}

private LdapPacket BuildMemberOfRequest(string userName, LdapAttribute[] memberFilter, int messageId)
{
var packet = new LdapPacket(messageId);

var baseDn = LdapProfile.GetBaseDn(userName);

var searchRequest = new LdapAttribute(LdapOperation.SearchRequest);
searchRequest.ChildAttributes.Add(new LdapAttribute(UniversalDataType.OctetString, baseDn)); //base dn
searchRequest.ChildAttributes.Add(new LdapAttribute(UniversalDataType.Enumerated, (byte)2)); //scope: subtree
searchRequest.ChildAttributes.Add(new LdapAttribute(UniversalDataType.Enumerated, (byte)0)); //aliases: never
searchRequest.ChildAttributes.Add(new LdapAttribute(UniversalDataType.Integer, (byte)0)); //size limit: unset
searchRequest.ChildAttributes.Add(new LdapAttribute(UniversalDataType.Integer, (byte)60)); //time limit: 60
searchRequest.ChildAttributes.Add(new LdapAttribute(UniversalDataType.Boolean, true)); //typesOnly: true

foreach (var attribute in memberFilter)
{
searchRequest.ChildAttributes.Add(attribute);
}

packet.ChildAttributes.Add(searchRequest);

var attrList = new LdapAttribute(UniversalDataType.Sequence);
attrList.ChildAttributes.Add(new LdapAttribute(UniversalDataType.OctetString, "distinguishedName"));

searchRequest.ChildAttributes.Add(attrList);

return packet;
}


private LdapAttribute[] GetFilter(string userName)
{
return new[]
{
new LdapAttribute((byte)LdapFilterChoice.equalityMatch)
{
ChildAttributes =
{
new LdapAttribute(UniversalDataType.OctetString, "member"),
new LdapAttribute(UniversalDataType.OctetString, userName)
}
}
};
}

private IEnumerable<string> GetSearchResult(LdapPacket packet)
{
var searchResults = new List<string>();

foreach (var searchResultEntry in packet.ChildAttributes.FindAll(attr => attr.LdapOperation == LdapOperation.SearchResultEntry))
{
if (searchResultEntry.ChildAttributes.Count > 0)
{
var result = searchResultEntry.ChildAttributes[0].GetValue<string>();
searchResults.Add(LdapService.FormatDn(result));
}
}

return searchResults;
}
}
9 changes: 9 additions & 0 deletions src/Services/MemberOf/IMemberOfService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.IO;
using System.Threading.Tasks;

namespace MultiFactor.Ldap.Adapter.Services.MemberOf;

public interface IMemberOfService
{
Task<bool> IsMemberOf(Stream ldapConnectedStream, LdapProfile profile, string groupDn, int messageId);
}