Also now loading the classrooms in adminstate
This commit is contained in:
@@ -54,6 +54,7 @@ else
|
|||||||
{
|
{
|
||||||
base.OnInitialized();
|
base.OnInitialized();
|
||||||
Dispatcher.Dispatch(new Wollmilchsau.Store.AdminStore.GetAllLdapUsers());
|
Dispatcher.Dispatch(new Wollmilchsau.Store.AdminStore.GetAllLdapUsers());
|
||||||
|
Dispatcher.Dispatch(new Wollmilchsau.Store.AdminStore.GetAllClassrooms());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task DownloadCsvAsync()
|
private Task DownloadCsvAsync()
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Wollmilchsau.Models;
|
||||||
|
|
||||||
|
public sealed record Classroom(string Name);
|
||||||
@@ -4,5 +4,8 @@ namespace Wollmilchsau.Services.Ldap;
|
|||||||
|
|
||||||
public interface ILdapService
|
public interface ILdapService
|
||||||
{
|
{
|
||||||
|
Task<int> GetUserCount(CancellationToken cancellationToken = default);
|
||||||
IAsyncEnumerable<LdapUser> GetAllUsers(CancellationToken cancellationToken = default);
|
IAsyncEnumerable<LdapUser> GetAllUsers(CancellationToken cancellationToken = default);
|
||||||
|
Task<int> GetClassroomCount(CancellationToken cancellationToken = default);
|
||||||
|
IAsyncEnumerable<Classroom> GetAllClassrooms(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,4 +27,10 @@ public sealed class LdapOptions
|
|||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public string TeacherRole { get; init; } = string.Empty;
|
public string TeacherRole { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string GroupBase { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string KlasseGroupFilter { get; init; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,21 @@ public sealed class LdapService(IOptions<LdapOptions> options) : ILdapService
|
|||||||
{
|
{
|
||||||
private readonly LdapOptions _options = options.Value;
|
private readonly LdapOptions _options = options.Value;
|
||||||
|
|
||||||
|
public Task<int> GetUserCount(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
using var connection = CreateConnection();
|
||||||
|
var request = new SearchRequest(
|
||||||
|
BuildDistinguishedName(_options.UserBase),
|
||||||
|
GetSupportedRolesFilter(),
|
||||||
|
SearchScope.Subtree,
|
||||||
|
"1.1");
|
||||||
|
var response = (SearchResponse)connection.SendRequest(request);
|
||||||
|
|
||||||
|
return Task.FromResult(response.Entries.Count);
|
||||||
|
}
|
||||||
|
|
||||||
public async IAsyncEnumerable<LdapUser> GetAllUsers(
|
public async IAsyncEnumerable<LdapUser> GetAllUsers(
|
||||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -24,6 +39,45 @@ public sealed class LdapService(IOptions<LdapOptions> options) : ILdapService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<int> GetClassroomCount(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
using var connection = CreateConnection();
|
||||||
|
var request = new SearchRequest(
|
||||||
|
BuildDistinguishedName(_options.GroupBase),
|
||||||
|
_options.KlasseGroupFilter,
|
||||||
|
SearchScope.Subtree,
|
||||||
|
"1.1");
|
||||||
|
var response = (SearchResponse)connection.SendRequest(request);
|
||||||
|
|
||||||
|
return Task.FromResult(response.Entries.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<Classroom> GetAllClassrooms(
|
||||||
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
using var connection = CreateConnection();
|
||||||
|
var request = new SearchRequest(
|
||||||
|
BuildDistinguishedName(_options.GroupBase),
|
||||||
|
_options.KlasseGroupFilter,
|
||||||
|
SearchScope.Subtree,
|
||||||
|
"cn");
|
||||||
|
var response = (SearchResponse)connection.SendRequest(request);
|
||||||
|
|
||||||
|
foreach (var entry in response.Entries.Cast<SearchResultEntry>())
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var commonName = GetFirstAttributeValue(entry, "cn")
|
||||||
|
?? throw new InvalidOperationException($"LDAP classroom entry '{entry.DistinguishedName}' is missing cn.");
|
||||||
|
yield return new Classroom(GetClassroomName(commonName));
|
||||||
|
await Task.Yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private LdapConnection CreateConnection()
|
private LdapConnection CreateConnection()
|
||||||
{
|
{
|
||||||
var ldapUri = new Uri(_options.LdapUri, UriKind.Absolute);
|
var ldapUri = new Uri(_options.LdapUri, UriKind.Absolute);
|
||||||
@@ -52,7 +106,7 @@ public sealed class LdapService(IOptions<LdapOptions> options) : ILdapService
|
|||||||
var teacherRoleDn = BuildDistinguishedName(_options.TeacherRole, _options.RoleBase);
|
var teacherRoleDn = BuildDistinguishedName(_options.TeacherRole, _options.RoleBase);
|
||||||
var request = new SearchRequest(
|
var request = new SearchRequest(
|
||||||
BuildDistinguishedName(_options.UserBase),
|
BuildDistinguishedName(_options.UserBase),
|
||||||
$"(|(memberOf={EscapeFilterValue(studentRoleDn)})(memberOf={EscapeFilterValue(teacherRoleDn)}))",
|
GetSupportedRolesFilter(studentRoleDn, teacherRoleDn),
|
||||||
SearchScope.Subtree,
|
SearchScope.Subtree,
|
||||||
"uid",
|
"uid",
|
||||||
"sn",
|
"sn",
|
||||||
@@ -102,6 +156,30 @@ public sealed class LdapService(IOptions<LdapOptions> options) : ILdapService
|
|||||||
throw new InvalidOperationException($"LDAP user entry '{entry.DistinguishedName}' is not a member of a supported role.");
|
throw new InvalidOperationException($"LDAP user entry '{entry.DistinguishedName}' is not a member of a supported role.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetSupportedRolesFilter()
|
||||||
|
{
|
||||||
|
var studentRoleDn = BuildDistinguishedName(_options.StudentsRole, _options.RoleBase);
|
||||||
|
var teacherRoleDn = BuildDistinguishedName(_options.TeacherRole, _options.RoleBase);
|
||||||
|
|
||||||
|
return GetSupportedRolesFilter(studentRoleDn, teacherRoleDn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetSupportedRolesFilter(string studentRoleDn, string teacherRoleDn)
|
||||||
|
{
|
||||||
|
return $"(|(memberOf={EscapeFilterValue(studentRoleDn)})(memberOf={EscapeFilterValue(teacherRoleDn)}))";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetClassroomName(string commonName)
|
||||||
|
{
|
||||||
|
const string classroomPrefix = "klasse.";
|
||||||
|
if (!commonName.StartsWith(classroomPrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"LDAP classroom cn '{commonName}' does not start with '{classroomPrefix}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return commonName[classroomPrefix.Length..];
|
||||||
|
}
|
||||||
|
|
||||||
private string BuildDistinguishedName(params string[] relativeNames)
|
private string BuildDistinguishedName(params string[] relativeNames)
|
||||||
{
|
{
|
||||||
return string.Join(',', relativeNames.Append(_options.BaseDN));
|
return string.Join(',', relativeNames.Append(_options.BaseDN));
|
||||||
|
|||||||
@@ -6,4 +6,6 @@ public sealed record GetAllMoodleUsers();
|
|||||||
public sealed record GetAllElementUsers();
|
public sealed record GetAllElementUsers();
|
||||||
public sealed record GetAllLdapUsers();
|
public sealed record GetAllLdapUsers();
|
||||||
public sealed record SetLdapUsers(IReadOnlyCollection<LdapUser> Users);
|
public sealed record SetLdapUsers(IReadOnlyCollection<LdapUser> Users);
|
||||||
|
public sealed record GetAllClassrooms();
|
||||||
|
public sealed record SetClassrooms(IReadOnlyCollection<Classroom> Classrooms);
|
||||||
public sealed record GetAllMsolUsers();
|
public sealed record GetAllMsolUsers();
|
||||||
|
|||||||
@@ -9,36 +9,99 @@ namespace Wollmilchsau.Store.AdminStore;
|
|||||||
public sealed class AdminEffects(ILdapService ldapService, IDistributedCache cache)
|
public sealed class AdminEffects(ILdapService ldapService, IDistributedCache cache)
|
||||||
{
|
{
|
||||||
private const string LdapUsersCacheKey = "admin:ldap-users";
|
private const string LdapUsersCacheKey = "admin:ldap-users";
|
||||||
|
private const string ClassroomsCacheKey = "admin:classrooms";
|
||||||
|
|
||||||
[EffectMethod]
|
[EffectMethod]
|
||||||
public async Task HandleGetAllLdapUsers(GetAllLdapUsers action, IDispatcher dispatcher)
|
public async Task HandleGetAllLdapUsers(GetAllLdapUsers action, IDispatcher dispatcher)
|
||||||
{
|
{
|
||||||
|
var ldapUserCount = await ldapService.GetUserCount();
|
||||||
var cachedUsers = await cache.GetStringAsync(LdapUsersCacheKey);
|
var cachedUsers = await cache.GetStringAsync(LdapUsersCacheKey);
|
||||||
IReadOnlyCollection<LdapUser> users;
|
IReadOnlyCollection<LdapUser> users;
|
||||||
|
|
||||||
if (cachedUsers is not null)
|
if (cachedUsers is not null)
|
||||||
{
|
{
|
||||||
users = JsonSerializer.Deserialize<LdapUser[]>(cachedUsers)
|
var deserializedUsers = JsonSerializer.Deserialize<LdapUser[]>(cachedUsers);
|
||||||
?? throw new InvalidOperationException("The cached LDAP users could not be deserialized.");
|
if (deserializedUsers is not null && deserializedUsers.Length == ldapUserCount)
|
||||||
|
{
|
||||||
|
users = deserializedUsers;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var loadedUsers = new List<LdapUser>();
|
await cache.RemoveAsync(LdapUsersCacheKey);
|
||||||
|
users = await LoadAndCacheLdapUsers(ldapUserCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
users = await LoadAndCacheLdapUsers(ldapUserCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatcher.Dispatch(new SetLdapUsers(users));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IReadOnlyCollection<LdapUser>> LoadAndCacheLdapUsers(int ldapUserCount)
|
||||||
|
{
|
||||||
|
var loadedUsers = new List<LdapUser>(ldapUserCount);
|
||||||
await foreach (var user in ldapService.GetAllUsers())
|
await foreach (var user in ldapService.GetAllUsers())
|
||||||
{
|
{
|
||||||
loadedUsers.Add(user);
|
loadedUsers.Add(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
users = loadedUsers;
|
|
||||||
await cache.SetStringAsync(
|
await cache.SetStringAsync(
|
||||||
LdapUsersCacheKey,
|
LdapUsersCacheKey,
|
||||||
JsonSerializer.Serialize(users),
|
JsonSerializer.Serialize(loadedUsers),
|
||||||
new DistributedCacheEntryOptions
|
new DistributedCacheEntryOptions
|
||||||
{
|
{
|
||||||
SlidingExpiration = TimeSpan.FromMinutes(10)
|
SlidingExpiration = TimeSpan.FromMinutes(10)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return loadedUsers;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatcher.Dispatch(new SetLdapUsers(users));
|
[EffectMethod]
|
||||||
|
public async Task HandleGetAllClassrooms(GetAllClassrooms action, IDispatcher dispatcher)
|
||||||
|
{
|
||||||
|
var ldapClassroomCount = await ldapService.GetClassroomCount();
|
||||||
|
var cachedClassrooms = await cache.GetStringAsync(ClassroomsCacheKey);
|
||||||
|
IReadOnlyCollection<Classroom> classrooms;
|
||||||
|
|
||||||
|
if (cachedClassrooms is not null)
|
||||||
|
{
|
||||||
|
var deserializedClassrooms = JsonSerializer.Deserialize<Classroom[]>(cachedClassrooms);
|
||||||
|
if (deserializedClassrooms is not null && deserializedClassrooms.Length == ldapClassroomCount)
|
||||||
|
{
|
||||||
|
classrooms = deserializedClassrooms;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await cache.RemoveAsync(ClassroomsCacheKey);
|
||||||
|
classrooms = await LoadAndCacheClassrooms(ldapClassroomCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
classrooms = await LoadAndCacheClassrooms(ldapClassroomCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatcher.Dispatch(new SetClassrooms(classrooms));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IReadOnlyCollection<Classroom>> LoadAndCacheClassrooms(int ldapClassroomCount)
|
||||||
|
{
|
||||||
|
var loadedClassrooms = new List<Classroom>(ldapClassroomCount);
|
||||||
|
await foreach (var classroom in ldapService.GetAllClassrooms())
|
||||||
|
{
|
||||||
|
loadedClassrooms.Add(classroom);
|
||||||
|
}
|
||||||
|
|
||||||
|
await cache.SetStringAsync(
|
||||||
|
ClassroomsCacheKey,
|
||||||
|
JsonSerializer.Serialize(loadedClassrooms),
|
||||||
|
new DistributedCacheEntryOptions
|
||||||
|
{
|
||||||
|
SlidingExpiration = TimeSpan.FromMinutes(10)
|
||||||
|
});
|
||||||
|
|
||||||
|
return loadedClassrooms;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,4 +14,15 @@ public static class AdminReducers
|
|||||||
LdapUsersLoaded = true
|
LdapUsersLoaded = true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[ReducerMethod]
|
||||||
|
public static AdminState ReduceSetClassrooms(AdminState state, SetClassrooms action)
|
||||||
|
{
|
||||||
|
return state with
|
||||||
|
{
|
||||||
|
Classrooms = action.Classrooms
|
||||||
|
.OrderBy(classroom => classroom.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToImmutableList()
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ public sealed record AdminState
|
|||||||
public ImmutableDictionary<Guid, ElementUser> ElementUsers { get; init; } = [];
|
public ImmutableDictionary<Guid, ElementUser> ElementUsers { get; init; } = [];
|
||||||
public ImmutableDictionary<Guid, MoodleUser> MoodleUsers { get; init; } = [];
|
public ImmutableDictionary<Guid, MoodleUser> MoodleUsers { get; init; } = [];
|
||||||
public ImmutableDictionary<string, LdapUser> LdapUsers { get; init; } = [];
|
public ImmutableDictionary<string, LdapUser> LdapUsers { get; init; } = [];
|
||||||
|
public ImmutableList<Classroom> Classrooms { get; init; } = [];
|
||||||
public bool LdapUsersLoaded { get; init; }
|
public bool LdapUsersLoaded { get; init; }
|
||||||
public ImmutableDictionary<Guid, MsolUser> MsolUsers { get; init; } = [];
|
public ImmutableDictionary<Guid, MsolUser> MsolUsers { get; init; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user