diff --git a/src/Controllers/GroupsController.cs b/src/Controllers/GroupsController.cs new file mode 100644 index 0000000..2debc38 --- /dev/null +++ b/src/Controllers/GroupsController.cs @@ -0,0 +1,117 @@ +using Berufsschule_HAM.Services; +using Microsoft.AspNetCore.Mvc; +using Novell.Directory.Ldap; +using Berufsschule_HAM.Models; +using System.Text.Json; + +[Route("[controller]")] +public class GroupsController : Controller +{ + private readonly LdapService _ldap; + private readonly ILogger _logger; + + public GroupsController(LdapService ldap, ILogger logger) + { + _ldap = ldap; + _logger = logger; + } + + [HttpGet("Index")] + public async Task> Index(GroupsIndexRequestModel requestModel) + { + string? cn = requestModel.Cn; + List attributes = [.. _ldap.GroupsAttributes]; + if (!requestModel.GidNumber) attributes.Remove("gidNumber"); + if (!requestModel.Permissions) attributes.Remove("description"); + IEnumerable groups; + if (cn is null) + { + groups = await _ldap.ListGroupsAsync([.. attributes]); + } + else + { + try + { + groups = [await _ldap.GetGroupByCnAsync(cn, [.. attributes])]; + } + catch (InvalidOperationException) + { + groups = []; + } + } + return groups; + } + + [HttpGet("Delete")] + public async Task Delete(string uid) + { + return await Task.Run(() => + { + try + { + _ldap.DeleteGroup(uid); + return true; + } + catch (Exception) + { + return false; + } + }); + } + + [HttpGet("Create")] + public async Task Create(string cn, string gidNumber, GroupPermission[] permissions, string description) + { + try + { + description ??= JsonSerializer.Serialize(new GroupPermissions() {Permissions = []}); + + LdapAttributeSet attributeSet = + [ + new LdapAttribute("objectClass", "posixGroup"), + new LdapAttribute("objectClass", "top"), + new LdapAttribute("cn", cn), + new LdapAttribute("gidNumber", gidNumber), + new LdapAttribute("description", + JsonSerializer.Serialize( + new GroupPermissions() + { + Permissions = [.. permissions] + })), + ]; + await _ldap.CreateGroup(cn, attributeSet); + return true; + } + catch (Exception ex) + { + _logger.LogError("Unable to create user: {ex.Message} - {ex.StackTrace}", [ex.Message, ex.StackTrace]); + return false; + } + } + + [HttpPost("Update")] + public async Task Update([FromBody]GroupsModifyRequestModel requestModel) + { + if (requestModel is null) + { + _logger.LogError("Unable to update a group because the GroupsModifyRequestModel is null"); + return false; + } + string cn = requestModel.Cn; + + if (requestModel.NewCn is not null) + { + await _ldap.UpdateGroup(cn, "cn", requestModel.NewCn); + cn = requestModel.NewCn; + } + if (requestModel.GidNumber is not null) + { + await _ldap.UpdateGroup(cn, "gidNumber", requestModel.GidNumber); + } + if (requestModel.Permissions is not null) + { + await _ldap.UpdateGroup(cn, "description", JsonSerializer.Serialize(requestModel.Permissions)); + } + return true; + } +} \ No newline at end of file diff --git a/src/Exceptions/ConfigurationExceptions.cs b/src/Exceptions/ConfigurationExceptions.cs new file mode 100644 index 0000000..f021265 --- /dev/null +++ b/src/Exceptions/ConfigurationExceptions.cs @@ -0,0 +1,3 @@ +namespace Berufsschule_HAM.Exceptions; + +public class GroupModelConfigurationException : Exception {} \ No newline at end of file diff --git a/src/Models/GroupModel.cs b/src/Models/GroupModel.cs new file mode 100644 index 0000000..c625c15 --- /dev/null +++ b/src/Models/GroupModel.cs @@ -0,0 +1,42 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Berufsschule_HAM.Exceptions; + +namespace Berufsschule_HAM.Models; + +public class GroupModel +{ + public required string Cn { get; set; } + public string? GidNumber { get; set; } + public List Permissions { get; set; } + public GroupModel(Dictionary ldapData) + { + Cn = ldapData.GetValueOrDefault("cn") ?? throw new GroupModelConfigurationException(); + GidNumber = ldapData.GetValueOrDefault("gidNumber"); + string? descriptionValue = ldapData.GetValueOrDefault("description"); + if (descriptionValue is null) + { + Permissions = []; + } + else + { + Permissions = JsonSerializer.Deserialize(descriptionValue)?.Permissions ?? []; + } + } +} + +public class GroupPermissions +{ + public required List Permissions { get; set; } +} + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum GroupPermission +{ + None, + CanInventorize, + CanManageUsers, + CanManageLocations, + CanManageAssets, + CanManageGroups +} \ No newline at end of file diff --git a/src/Models/GroupsRequestModels.cs b/src/Models/GroupsRequestModels.cs new file mode 100644 index 0000000..38a78f5 --- /dev/null +++ b/src/Models/GroupsRequestModels.cs @@ -0,0 +1,16 @@ +namespace Berufsschule_HAM.Models; + +public class GroupsIndexRequestModel +{ + public string? Cn { get; set; } + public bool GidNumber { get; set; } = true; + public bool Permissions { get; set; } = true; +} + +public class GroupsModifyRequestModel +{ + public required string Cn { get; set; } + public string? NewCn { get; set; } = null; + public string? GidNumber { get; set; } = null; + public GroupPermissions? Permissions { get; set; } = null; +} \ No newline at end of file diff --git a/src/Program.cs b/src/Program.cs index f5cd5b1..d28bfb8 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authentication.Cookies; using Berufsschule_HAM.Services; using Berufsschule_HAM.Models; using Berufsschule_HAM.HealthChecks; +using System.Text.Json.Serialization; var builder = WebApplication.CreateBuilder(args); // Bind options @@ -14,7 +15,9 @@ builder.Services.AddLocalization(options => options.ResourcesPath = "Resources") builder.Services.AddControllersWithViews() .AddViewLocalization(Microsoft.AspNetCore.Mvc.Razor.LanguageViewLocationExpanderFormat.Suffix) - .AddDataAnnotationsLocalization(); + .AddDataAnnotationsLocalization() + .AddJsonOptions(options => + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); diff --git a/src/Services/LdapService.cs b/src/Services/LdapService.cs index 47b2506..8930cda 100644 --- a/src/Services/LdapService.cs +++ b/src/Services/LdapService.cs @@ -45,7 +45,7 @@ public partial class LdapService : IDisposable public string UsersBaseDn => string.IsNullOrEmpty(_opts.UsersOu) ? _opts.BaseDn : $"{_opts.UsersOu},{_opts.BaseDn}"; public string GroupsBaseDn => string.IsNullOrEmpty(_opts.GroupsOu) ? _opts.BaseDn : $"{_opts.GroupsOu},{_opts.BaseDn}"; public string MigrationsBaseDn => string.IsNullOrEmpty(_opts.MigrationsOu) ? _opts.BaseDn : $"{_opts.MigrationsOu},{_opts.BaseDn}"; - + public string[] GroupsAttributes => ["cn", "gidNumber", "description"]; public async Task>> ListLocationsAsync() { return await ListObjectBy(LocationsBaseDn, "(ou=locations)", ["l", "street", "description"]); @@ -107,14 +107,34 @@ public partial class LdapService : IDisposable return returnValue; } + public async Task> ListGroupsAsync(string[] attributes) + { + List returnValue = []; + (await ListObjectBy(GroupsBaseDn, "", attributes)) + .ToList() + .ForEach(x => + returnValue.Add( + new GroupModel(x) + { + Cn = x["cn"] + } + ) + ); + return returnValue; + } public async Task GetUserByUidAsync(string uid) { - return new UserModel((await ListObjectBy(UsersBaseDn, $"uid={uid}", ["cn", "sn", "title", "uid", "jpegPhoto", "userPassword", "description"])).First()) {Uid = uid}; + return new UserModel((await ListObjectBy(UsersBaseDn, $"uid={uid}", ["cn", "sn", "title", "uid", "jpegPhoto", "userPassword", "description"])).First()) { Uid = uid }; } public async Task GetUserByUidAsync(string uid, string[] attributes) { - return new UserModel((await ListObjectBy(UsersBaseDn, $"uid={uid}", attributes)).First()) {Uid = uid}; + return new UserModel((await ListObjectBy(UsersBaseDn, $"uid={uid}", attributes)).First()) { Uid = uid }; + } + + public async Task GetGroupByCnAsync(string cn, string[] attributes) + { + return new GroupModel((await ListObjectBy(GroupsBaseDn, $"cn={cn}", attributes)).First()) { Cn = cn }; } @@ -129,6 +149,12 @@ public partial class LdapService : IDisposable await CreateObject(dn, attributeSet); } + public async Task CreateGroup(string cn, LdapAttributeSet attributeSet) + { + string dn = PrependRDN($"cn={cn}", GroupsBaseDn); + await CreateObject(dn, attributeSet); + } + public async Task CreateAsset(LdapAttributeSet attributeSet) { await CreateObject(AssetsBaseDn, attributeSet); @@ -228,13 +254,30 @@ public partial class LdapService : IDisposable string dn = PrependRDN($"uid={uid}", UsersBaseDn); DeleteObjectByDn(dn); } + + public void DeleteGroup(string cn) + { + string dn = PrependRDN($"cn={cn}", GroupsBaseDn); + DeleteObjectByDn(dn); + } + public async Task UpdateUser(string uid, string attributeName, string attributeValue) + { + await UpdateObject(UsersBaseDn, "uid", uid, attributeName, attributeValue); + } + + public async Task UpdateGroup(string cn, string attributeName, string attributeValue) + { + await UpdateObject(GroupsBaseDn, "cn", cn, attributeName, attributeValue); + } + + public async Task UpdateObject(string baseDn, string rdnKey, string rdnValue, string attributeName, string attributeValue) { await ConnectAndBind(); - string dn = PrependRDN($"uid={uid}", UsersBaseDn); - if (attributeName == "uid") + string dn = PrependRDN($"{rdnKey}={rdnValue}", baseDn); + if (attributeName == rdnKey) { - await _conn.RenameAsync(dn, $"uid={attributeValue}", true); + await _conn.RenameAsync(dn, $"{rdnKey}={attributeValue}", true); } else {