diff --git a/BLAZAM.sln b/BLAZAM.sln index 680a6618d..29f985c30 100644 --- a/BLAZAM.sln +++ b/BLAZAM.sln @@ -46,8 +46,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BLAZAMJobs", "BLAZAMJobs\BL EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlaywrightTests", "PlaywrightTests\PlaywrightTests.csproj", "{7E4AD00B-CA62-4994-A80E-A3900FF98E8E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BLAZAMNav", "BLAZAMNav\BLAZAMNav.csproj", "{EB80E5D9-D0A9-4808-AC66-F0094615EAE1}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -286,18 +284,6 @@ Global {7E4AD00B-CA62-4994-A80E-A3900FF98E8E}.Release|x64.Build.0 = Release|Any CPU {7E4AD00B-CA62-4994-A80E-A3900FF98E8E}.Release|x86.ActiveCfg = Release|Any CPU {7E4AD00B-CA62-4994-A80E-A3900FF98E8E}.Release|x86.Build.0 = Release|Any CPU - {EB80E5D9-D0A9-4808-AC66-F0094615EAE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EB80E5D9-D0A9-4808-AC66-F0094615EAE1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EB80E5D9-D0A9-4808-AC66-F0094615EAE1}.Debug|x64.ActiveCfg = Debug|Any CPU - {EB80E5D9-D0A9-4808-AC66-F0094615EAE1}.Debug|x64.Build.0 = Debug|Any CPU - {EB80E5D9-D0A9-4808-AC66-F0094615EAE1}.Debug|x86.ActiveCfg = Debug|Any CPU - {EB80E5D9-D0A9-4808-AC66-F0094615EAE1}.Debug|x86.Build.0 = Debug|Any CPU - {EB80E5D9-D0A9-4808-AC66-F0094615EAE1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EB80E5D9-D0A9-4808-AC66-F0094615EAE1}.Release|Any CPU.Build.0 = Release|Any CPU - {EB80E5D9-D0A9-4808-AC66-F0094615EAE1}.Release|x64.ActiveCfg = Release|Any CPU - {EB80E5D9-D0A9-4808-AC66-F0094615EAE1}.Release|x64.Build.0 = Release|Any CPU - {EB80E5D9-D0A9-4808-AC66-F0094615EAE1}.Release|x86.ActiveCfg = Release|Any CPU - {EB80E5D9-D0A9-4808-AC66-F0094615EAE1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/BLAZAM/App.razor b/BLAZAM/App.razor index 29a700290..b65821195 100644 --- a/BLAZAM/App.razor +++ b/BLAZAM/App.razor @@ -1,13 +1,16 @@ @inject IHttpContextAccessor context @inject IApplicationUserStateService userStateService @inject ICurrentUserStateService currentUser - - +@{ + var dsad = 3; +} + + @@ -17,7 +20,6 @@ @if (context.User.Identity?.IsAuthenticated != true) { - Login @@ -31,7 +33,6 @@ You are not authorized to access this resource. } - @@ -42,6 +43,8 @@ + + @@ -55,13 +58,13 @@ - - - - - - + + + + + + @code { bool darkMode = false; @@ -84,7 +87,7 @@ activeTheme = new BlueTheme(); } - + } darkMode = currentUser.State?.Preferences?.DarkMode == true; } diff --git a/BLAZAM/BLAZAM - Backup.csproj b/BLAZAM/BLAZAM - Backup.csproj deleted file mode 100644 index d3f8fdfae..000000000 --- a/BLAZAM/BLAZAM - Backup.csproj +++ /dev/null @@ -1,142 +0,0 @@ - - - - net8.0 - enable - enable - false - 1.0.7 - 2024.11.02.0114 - false - BLAZAM - False - - - - - - - - - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - - - - True - True - Resources.resx - - - - - - Never - - - PreserveNewest - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - - - - - - PreserveNewest - - - PreserveNewest - - - - - - - diff --git a/BLAZAM/BLAZAM.csproj b/BLAZAM/BLAZAM.csproj index b335af3c2..4b94afb7a 100644 --- a/BLAZAM/BLAZAM.csproj +++ b/BLAZAM/BLAZAM.csproj @@ -5,11 +5,11 @@ enable enable false - 1.1.0 - 2024.11.13.2234 + 1.2.0 + 2024.11.25.0412 false BLAZAM - False + True @@ -57,8 +57,9 @@ - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -66,14 +67,18 @@ - + + - - - + + + + + + @@ -103,6 +108,10 @@ + + + + True diff --git a/BLAZAM/Helpers/Helpers.cs b/BLAZAM/Helpers/Helpers.cs new file mode 100644 index 000000000..a7225dc3e --- /dev/null +++ b/BLAZAM/Helpers/Helpers.cs @@ -0,0 +1,47 @@ +using BLAZAM.ActiveDirectory.Interfaces; +using BLAZAM.Common.Data; +using BLAZAM.Database.Models.Templates; +using BLAZAM.EmailMessage.Email.Notifications; +using MudBlazor; +using System.Security; + +namespace BLAZAM.Helpers +{ + public static class Helpers + { + public static IADUser GenerateTemplateUser(this DirectoryTemplate template, NewUserName newUserName, IActiveDirectoryContext directory) + { + IADUser? newUser; + var ou = directory.OUs.FindOuByString(template.EffectiveParentOU).FirstOrDefault(); + if (ou == null) throw new ApplicationException("OU could not be found for new user"); + var displayName = template.GenerateDisplayName(newUserName); + newUser = ou.CreateUser(displayName); + + newUser.SamAccountName = template.GenerateUsername(newUserName); + newUser.DisplayName = displayName; + //newUser.SetPassword(template.GeneratePassword().ToSecureString(),false); + //newUser.CanonicalName = template.GenerateDisplayName(newUserName); + newUser.StagePasswordChange(template.GeneratePassword(newUserName).ToSecureString()); + if (template.EffectiveRequirePasswordChange == true) + newUser.StageRequirePasswordChange(true); + if (!newUserName.GivenName.IsNullOrEmpty()) + newUser.GivenName = newUserName.GivenName; + if (!newUserName.MiddleName.IsNullOrEmpty()) + newUser.MiddleName = newUserName.MiddleName; + if (!newUserName.Surname.IsNullOrEmpty()) + newUser.Surname = newUserName.Surname; + + + + template.EffectiveAssignedGroupSids.ForEach(sid => + { + var group = directory.Groups.FindGroupBySID(sid.GroupSid); + if (group != null) + newUser.AssignTo(group); + + }); + return newUser; + } + + } +} diff --git a/BLAZAM/Pages/API/Data/NewUserDetails.cs b/BLAZAM/Pages/API/Data/NewUserDetails.cs new file mode 100644 index 000000000..3bf3d9816 --- /dev/null +++ b/BLAZAM/Pages/API/Data/NewUserDetails.cs @@ -0,0 +1,72 @@ +using BLAZAM.Database.Models.Templates; +using System.Text.Json; + +namespace BLAZAM.Pages.API.Data +{ + /// + /// Request package for creation of a templated user + /// + public class NewUserDetails + { + /// + /// The given name for this user + /// + public string FirstName { get; set; } + /// + /// The middle name for this user + /// + public string? MiddleName { get; set; } + /// + /// The surname for this user + /// + public string? LastName { get; set; } + /// + /// If set, overrides the template generated username + /// + public string? Username { get; set; } + /// + /// The Distinguished Name of the Organizational Unit to + /// create the new user under + /// + /// + /// Only used for custom user creation, ignored for API template execution + /// + public string? OU { get; set; } + + /// + /// The fields to set for this user. Template field with values will also + /// be applied. + /// + public List? Fields { get; set; } + /// + /// A list of group SID's to assign for this user. Template groups will also + /// be applied. + /// + public List? Groups { get; set; } + + /// + /// If the template is set to send a welcome email, and requests a destination, will be sent + /// to this email address. + /// + public string? SendWelcomeEmailTo { get; set; } + } + public class NewUserDetailsExample + { + public object GetExamples() + { + return new NewUserDetails + { + FirstName = "John", + LastName = "Doe", + Username = "johndoe", + OU = "OU=Users,DC=example,DC=com", + Fields = new List + { + new NewUserField { FieldName = "Department", FieldValue = "Sales" }, + new NewUserField { FieldName = "Title", FieldValue = "Sales Representative" } + }, + Groups = new List { "S-1-5-21-1004336348-1177238915-682003330-512", "S-1-5-21-1004336348-1148567915-615476330-495" } + }; + } + } +} diff --git a/BLAZAM/Pages/API/Data/NewUserField.cs b/BLAZAM/Pages/API/Data/NewUserField.cs new file mode 100644 index 000000000..2e3ea1e5b --- /dev/null +++ b/BLAZAM/Pages/API/Data/NewUserField.cs @@ -0,0 +1,19 @@ +using System.Text.Json; + +namespace BLAZAM.Pages.API.Data +{ + /// + /// Represents an Active Directory attribute field to set for thee new user + /// + public class NewUserField + { + /// + /// The attribute name as set in Active Directory + /// + public string FieldName { get; set; } + /// + /// The value to set for this attribute field + /// + public object? FieldValue { get; set; } + } +} \ No newline at end of file diff --git a/BLAZAM/Pages/API/Token.cshtml b/BLAZAM/Pages/API/Token.cshtml deleted file mode 100644 index bea56a89a..000000000 --- a/BLAZAM/Pages/API/Token.cshtml +++ /dev/null @@ -1,4 +0,0 @@ -@page -@model BLAZAM.Server.Pages.API.TokenModel -@{ -} diff --git a/BLAZAM/Pages/API/Token.cshtml.cs b/BLAZAM/Pages/API/Token.cshtml.cs deleted file mode 100644 index b8a5f63be..000000000 --- a/BLAZAM/Pages/API/Token.cshtml.cs +++ /dev/null @@ -1,50 +0,0 @@ -using BLAZAM.Common.Data; -using BLAZAM.Common.Data.Database; -using BLAZAM.Database.Context; -using BLAZAM.Database.Models.User; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; -using Microsoft.IdentityModel.Tokens; -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; - -namespace BLAZAM.Server.Pages.API -{ - [Obsolete("Not using any local REST API")] - public class TokenModel : PageModel - { - public JwtSecurityTokenHandler JwtTokenHandler { get; private set; } - public string Token { get; private set; } - public IDatabaseContext Context { get; private set; } - - - public TokenModel(IDatabaseContext context) - { - Context = context; - } - - public JsonResult OnGet() - { - JwtTokenHandler = new JwtSecurityTokenHandler(); - var user = this.User.Identity?.Name; - if (string.IsNullOrEmpty(user)) - { - throw new InvalidOperationException("Name is not specified."); - } - - var claims = new[] { new Claim(ClaimTypes.Name, user) }; - var credentials = new SigningCredentials(ApplicationInfo.tokenKey, SecurityAlgorithms.HmacSha256); - var token = new JwtSecurityToken("ExampleServer", "ExampleClients", claims, expires: DateTime.Now.AddSeconds(60), signingCredentials: credentials); - Token = JwtTokenHandler.WriteToken(token); - var userSettings = Context.UserSettings.Where(u => u.UserGUID == this.User.Identity.Name).FirstOrDefault(); - if (userSettings != null) - { - userSettings.APIToken = Token; - } - - Context.SaveChanges(); - return new JsonResult(userSettings); - } - } -} diff --git a/BLAZAM/Pages/API/ValidateUpdateToken.cshtml b/BLAZAM/Pages/API/ValidateUpdateToken.cshtml deleted file mode 100644 index 96d98e309..000000000 --- a/BLAZAM/Pages/API/ValidateUpdateToken.cshtml +++ /dev/null @@ -1,4 +0,0 @@ -@page -@model BLAZAM.Server.Pages.API.ValidateUpdateTokenModel -@{ -} diff --git a/BLAZAM/Pages/API/ValidateUpdateToken.cshtml.cs b/BLAZAM/Pages/API/ValidateUpdateToken.cshtml.cs deleted file mode 100644 index 62200d55b..000000000 --- a/BLAZAM/Pages/API/ValidateUpdateToken.cshtml.cs +++ /dev/null @@ -1,19 +0,0 @@ -using BLAZAM.Server.Data.Services; -using BLAZAM.Services; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace BLAZAM.Server.Pages.API -{ - public class ValidateUpdateTokenModel : PageModel - { - - public IActionResult OnGet(string updateToken) - { - if (updateToken.Equals(AdminTokenService.Token.Guid.ToString())) - return new OkResult(); - - return new UnauthorizedResult(); - } - } -} diff --git a/BLAZAM/Pages/API/v1/ApiController.cs b/BLAZAM/Pages/API/v1/ApiController.cs new file mode 100644 index 000000000..cc4d9b8e6 --- /dev/null +++ b/BLAZAM/Pages/API/v1/ApiController.cs @@ -0,0 +1,63 @@ +using BLAZAM.ActiveDirectory.Interfaces; +using BLAZAM.Common.Data; +using BLAZAM.Database.Context; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; +using Microsoft.EntityFrameworkCore; +using BLAZAM.Services.Audit; +using BLAZAM.Session.Interfaces; + +namespace BLAZAM.Pages.API.v1 +{ + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = UserRoles.Login)] + [ApiController] + [Produces("application/json")] + [Route("api/v1/[controller]")] + public class ApiController : Controller + { + private DateTime _startTime = DateTime.Now; + protected Dictionary ResponseData = new(); + protected readonly IAppDatabaseFactory DbFactory; + protected readonly AuditLogger AuditLogger; + protected readonly IApplicationUserStateService UserStateService; + + protected IApplicationUserState? CurrentUserState { get; } + + public ApiController(IApplicationUserStateService applicationUserStateService, AuditLogger audit, IAppDatabaseFactory appDatabaseFactory, IHttpContextAccessor httpContextAccessor, IActiveDirectoryContextFactory adFactory) + { + //User = httpContextAccessor.HttpContext.User; + AuditLogger = audit; + UserStateService = applicationUserStateService; + CurrentUserState = UserStateService.CurrentUserState; + + Directory = adFactory.CreateActiveDirectoryContext(); + DbFactory = appDatabaseFactory; + RequestId = Guid.NewGuid(); + ResponseData.Add("Request Id", RequestId); + ResponseData.Add("Version", "1.0"); + ResponseData.Add("Received Time", _startTime); + ResponseData.Add("User", httpContextAccessor?.HttpContext?.User?.Identity?.Name); + ResponseData.Add("User Id", httpContextAccessor?.HttpContext?.User?.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Sid)?.Value); + ResponseData.Add("IP Address", httpContextAccessor?.HttpContext?.Connection?.RemoteIpAddress?.ToString()); + + } + //[HttpGet("badrequest")] // Add a route attribute + //public IActionResult BadRequest() + //{ + // return new BadRequestResult(); + //} + protected IActiveDirectoryContext Directory { get; } + protected Guid RequestId { get; } + + protected IActionResult FormatData(dynamic data) + { + ResponseData.Add("Data", data); + ResponseData.Add("Finish Time", DateTime.Now.ToString()); + ResponseData.Add("Runtime", (DateTime.Now - _startTime).TotalMilliseconds + "ms"); + + return new JsonResult(ResponseData); + } + } +} diff --git a/BLAZAM/Pages/API/v1/Search.cs b/BLAZAM/Pages/API/v1/Search.cs new file mode 100644 index 000000000..fec7e303b --- /dev/null +++ b/BLAZAM/Pages/API/v1/Search.cs @@ -0,0 +1,46 @@ +using BLAZAM.ActiveDirectory.Interfaces; +using BLAZAM.ActiveDirectory.Searchers; +using BLAZAM.Database.Context; +using BLAZAM.Services.Audit; +using BLAZAM.Session.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace BLAZAM.Pages.API.v1 +{ + /// + /// Searches Active Directory. + /// + [Produces("application/json")] + public class Search : ApiController + { + public Search(IApplicationUserStateService applicationUserStateService, AuditLogger audit, IAppDatabaseFactory appDatabaseFactory, IHttpContextAccessor httpContextAccessor, IActiveDirectoryContextFactory adFactory) : base(applicationUserStateService, audit, appDatabaseFactory, httpContextAccessor, adFactory) + { + } + + + + /// + /// Run a general search term query against all AD object types. + /// + /// + /// Sample request: + /// + /// GET /api/v1/search?query=fragment + /// + /// + /// The string fragment to search for + /// Returns a list of matching Active Directory objects. + /// Unauthorized - The user is not authenticated. + /// Forbidden - The user does not have the required role. + [HttpGet] + public IActionResult OnGet([FromQuery] string query) + { + ADSearch search = new ADSearch(Directory); + search.GeneralSearchTerm = query; + var data = search.Search(); + var data2 = data.Where(de => de.CanRead).ToList(); + var data3 = data2.Select(de => de.CanonicalName).ToList(); + return FormatData(data3); + } + } +} diff --git a/BLAZAM/Pages/API/v1/Templates.cs b/BLAZAM/Pages/API/v1/Templates.cs new file mode 100644 index 000000000..5dd9d4e42 --- /dev/null +++ b/BLAZAM/Pages/API/v1/Templates.cs @@ -0,0 +1,268 @@ +using BLAZAM.ActiveDirectory.Interfaces; +using BLAZAM.Database.Context; +using BLAZAM.Pages.API.Data; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using System.Text.Json.Serialization; +using Microsoft.EntityFrameworkCore; +using AngleSharp.Html.Construction; +using BLAZAM.Common.Data; +using Octokit; +using BLAZAM.EmailMessage.Email.Notifications; +using MudBlazor; +using System.Security; +using Microsoft.Extensions.Localization; +using BLAZAM.Localization; +using BLAZAM.Database.Models.Notifications; +using BLAZAM.Database.Models.Templates; +using BLAZAM.Jobs; +using BLAZAM.Services.Audit; +using BLAZAM.Session.Interfaces; +using System.Text.Json; + +namespace BLAZAM.Pages.API.v1 +{ + public class Templates : ApiController + { + private IAppDatabaseFactory _appDatabaseFactory; + private IStringLocalizer AppLocalization; + private EmailService EmailService; + private NotificationGenerationService OUNotificationService; + + public Templates(NotificationGenerationService ouNotificationService, EmailService email, IApplicationUserStateService applicationUserStateService, IStringLocalizer localizer, AuditLogger audit, IAppDatabaseFactory appDatabaseFactory, IHttpContextAccessor httpContextAccessor, IActiveDirectoryContextFactory adFactory) : base(applicationUserStateService, audit, appDatabaseFactory, httpContextAccessor, adFactory) + { + AppLocalization = localizer; + EmailService = email; + OUNotificationService = ouNotificationService; + } + + + + /// + /// Executes a user creation template. Any required fields will need to be provided in form data. + /// + /// /// + /// Sample request: + /// + /// POST /api/v1/templates/execute/2 + /// { + /// "firstName": "Test", + /// "lastName": "User", + /// "fields": [ + /// { + /// "FieldName": "l", + /// "FieldValue": "Boston" + /// } + /// ] + /// "groups": [ + /// { + /// "S-1-5-21-1004336348-1177238915-682003330-512" + /// } + /// ] + /// } + /// + /// + /// The ID of the template to execute. + /// A complete NewUserDetails request schema + /// Returns the DN of the created user. + /// Unauthorized - The user is not authenticated. + /// Forbidden - The user does not have the required role. + /// Unprocessable - The creation request cannot be processed due to an internal error. + [HttpPost] + [Route("/api/v1/templates/execute/{templateId}")] + + public async Task Execute(int templateId, [FromBody] NewUserDetails newUserDetails) + { + //newUserDetails.Fields.Add(new() { FieldName = "test", FieldValue = "val" }); + //var test = JsonConvert.SerializeObject(newUserDetails); + + var context = await DbFactory.CreateDbContextAsync(); + var template = await context.DirectoryTemplates.Include(t => t.ParentTemplate).FirstOrDefaultAsync(t => t.Id == templateId); + + if (template != null) + { + if (template.HasRequiredFields()) + { + var requiredFields = template.EffectiveFieldValues.Where(fv => fv.Required).ToList(); + foreach (var field in requiredFields) + { + if (!newUserDetails.Fields.Any(f => f.FieldName.Equals(field.FieldName, StringComparison.InvariantCultureIgnoreCase))) + { + return new BadRequestObjectResult(field.FieldName + " is a required field"); + } + + } + } + + var newUserName = new NewUserName() + { + GivenName = newUserDetails.FirstName, + MiddleName = newUserDetails.MiddleName, + Surname = newUserDetails.LastName + }; + + var newUser = template.GenerateTemplateUser(newUserName, Directory); + if (!newUserDetails.Username.IsNullOrEmpty()) + { + newUser.SamAccountName = newUserDetails.Username; + } + var password = newUser.NewPassword.ToPlainText().ToSecureString(); + foreach (var fieldValue in template.EffectiveFieldValues) + { + try + { + if (fieldValue.Field != null && fieldValue.Value != null) + if (fieldValue.Field.FieldName.ToLower() == "homedirectory") + newUser.HomeDirectory = template.ReplaceVariables(fieldValue.Value, newUserName, newUser.SamAccountName); + else + newUser.NewEntryProperties[fieldValue.Field.FieldName] = template.ReplaceVariables(fieldValue.Value, newUserName, newUser.SamAccountName); + else if (fieldValue.CustomField != null && fieldValue.Value != null) + newUser.NewEntryProperties[fieldValue.CustomField.FieldName] = template.ReplaceVariables(fieldValue.Value, newUserName, newUser.SamAccountName); + } + catch (Exception ex) + { + Loggers.ActiveDirectoryLogger.Error("Could not set value for " + fieldValue.Field?.FieldName + ": " + fieldValue.Value?.ToString() + " {@Error}", ex); + } + + } + if (newUserDetails.Fields != null) + { + foreach (var field in newUserDetails.Fields) + { + var json = field.FieldValue as JsonElement?; + var kind = json.Value.ValueKind; + object? value=null; + switch (kind) + { + case JsonValueKind.String: + value = json.Value.GetString(); break; + case JsonValueKind.Number: + value = json.Value.GetDouble(); break; + case JsonValueKind.False: + case JsonValueKind.True: + value = json.Value.GetBoolean(); break; + + } + newUser.SetCustomProperty(field.FieldName,value) ; + } + } + if (newUserDetails.Groups != null) + { + foreach (var groupSid in newUserDetails.Groups) + { + var group = (IADGroup)Directory.GetDirectoryEntryByDN(groupSid); + if (group != null) + { + newUser.AssignTo(group); + + } + else + { + + } + } + } + + + IJob createUserJob = new Job(AppLocalization["Create User"]); + createUserJob.StopOnFailedStep = true; + //createUserJob.ShowJobDetailsDialog(MessageService); + //_username = User.SamAccountName; + //_userPassword = User.NewPassword; + var result = await newUser.CommitChangesAsync(createUserJob); + if (result.FailedSteps.Count == 0) + { + newUser = (IADUser)Directory.GetDirectoryEntryByDN(newUser.DN); + await AuditLogger.User.Created(newUser); + + _ = OUNotificationService.PostAsync(newUser, NotificationType.Create, CurrentUserState); + + try + { + if (template?.EffectiveSendWelcomeEmail == true) + { + if (template.EffectiveAskForAlternateEmail == true || newUser.Email.IsNullOrEmpty()) + { + + await SendWelcomeEmail(newUser, newUserDetails.SendWelcomeEmailTo, password); + + } + else + { + await SendWelcomeEmail(newUser, newUser.Email, password); + } + } + } + catch + { + + } + return new CreatedResult(newUser.OU, newUser.DN); + } + else + { + return new UnprocessableEntityObjectResult(result.FailedSteps.Select(s => s.Exception.InnerException != null ? s.Exception.InnerException.Message : s.Exception.Message)); + } + } + else + { + return new NotFoundObjectResult(templateId); + } + return new BadRequestResult(); + } + + + /// + /// Returns all user creation templates the user has access to. + /// + /// Returns a list of user creation templates. + /// Unauthorized - The user is not authenticated. + /// Forbidden - The user does not have the required role. + [HttpGet] + [Route("/api/v1/templates/list/")] + public IActionResult List() + { + using var context = DbFactory.CreateDbContext(); + var list = context.DirectoryTemplates.Where(t => t.DeletedAt == null && t.Visible).ToList(); + return FormatData(list); + + } + + private void Add(dynamic data, string title, string key) + { + var raw = User.Claims.FirstOrDefault(x => x.Type == key)?.Value; + + var str = raw?.ToString(); + data.Add(title, str); + } + private void AddDateTime(dynamic data, string title, string key) + { + var raw = User.Claims.FirstOrDefault(x => x.Type == key)?.Value; + var lng = long.Parse(raw); + + + var dt = DateTime.UnixEpoch.AddSeconds(lng); ; + var str = dt.ToString(); + data.Add(title, str); + } + + async Task SendWelcomeEmail(IADUser user, string to, SecureString password) + { + try + { + NewUserWelcomeEmailMessage message = new NewUserWelcomeEmailMessage(); + message.Domain = user.Directory.ConnectionSettings?.FQDN; + message.Username = user.SamAccountName; + message.Password = password; + var html = message.Render(); + await EmailService.SendMessage(AppLocalization["New Account Details"], message, to); + + } + catch (Exception ex) + { + Loggers.SystemLogger.Error("Error sending welcome email {@Error}", ex); + } + //emailPreview = EmailService.PrepareHTMLForEmail(html); + } + } +} diff --git a/BLAZAM/Pages/API/v1/Test.cs b/BLAZAM/Pages/API/v1/Test.cs new file mode 100644 index 000000000..6841ee53b --- /dev/null +++ b/BLAZAM/Pages/API/v1/Test.cs @@ -0,0 +1,66 @@ +using BLAZAM.ActiveDirectory.Interfaces; +using BLAZAM.Common.Data; +using BLAZAM.Database.Context; +using BLAZAM.Services.Audit; +using BLAZAM.Session.Interfaces; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace BLAZAM.Pages.API.v1 +{ + public class Test : ApiController + { + public Test(IApplicationUserStateService applicationUserStateService, AuditLogger audit, IAppDatabaseFactory appDatabaseFactory, IHttpContextAccessor httpContextAccessor, IActiveDirectoryContextFactory adFactory) : base(applicationUserStateService, audit, appDatabaseFactory, httpContextAccessor, adFactory) + { + } + + + + /// + /// API connection check to test the configuration. + /// + /// + /// Returns details about the user performing the test. + /// Unauthorized - The user is not authenticated. + /// Forbidden - The user does not have the required role. + [HttpGet] + public IActionResult OnGet() + { + dynamic data = new Dictionary(); + Add(data, "Issuer", "iss"); + + data.Add("Username", User.Identity?.Name); + AddDateTime(data, "Not Before", "nbf"); + AddDateTime(data, "Issued At", "iat"); + AddDateTime(data, "Expires", "exp"); + var claims = new List>(); + foreach (var claim in User.Claims) + { + claims.Add(new(claim.Type, claim.Value)); + } + data.Add("Claims", claims); + return FormatData(data); + } + + + + private void Add(dynamic data, string title, string key) + { + var raw = User.Claims.FirstOrDefault(x => x.Type == key)?.Value; + + var str = raw?.ToString(); + data.Add(title, str); + } + private void AddDateTime(dynamic data, string title, string key) + { + var raw = User.Claims.FirstOrDefault(x => x.Type == key)?.Value; + var lng = long.Parse(raw); + + + var dt = DateTime.UnixEpoch.AddSeconds(lng); ; + var str = dt.ToString(); + data.Add(title, str); + } + } +} diff --git a/BLAZAM/Pages/Configure/ApiTokens.razor b/BLAZAM/Pages/Configure/ApiTokens.razor new file mode 100644 index 000000000..9722a0725 --- /dev/null +++ b/BLAZAM/Pages/Configure/ApiTokens.razor @@ -0,0 +1,70 @@ +@page "/api-tokens" +@attribute [Authorize(Roles = UserRoles.SuperAdmin)] + +@inherits DatabaseComponentBase +@AppLocalization["API Tokens"] + + + + + + + + @AppLocalization["API Tokens"] + + + + + + +@code { + List _tokens; + bool _showDeleted; + + bool ShowDeleted + { + get => _showDeleted; set + { + if (_showDeleted == value) return; + _showDeleted = value; + _=GetTokens(); + + } + } + protected override void OnInitialized() + { + base.OnInitialized(); + _=GetTokens(); + } + public async Task GetTokens() + { + LoadingData = true; + if (_showDeleted) + _tokens = await Context.ApiTokens.Include(t=>t.User).ToListAsync(); + else + _tokens = await Context.ApiTokens.Where(t => t.DeletedAt == null).Include(t => t.User).ToListAsync(); + LoadingData = false; + + } + public async Task Save() + { + await Context.SaveChangesAsync(); + SnackBarService.Success(AppLocalization["Changes have been saved"]); + } + + private async Task Delete(ApiToken? tokenToDelete) + { + if (await MessageService.Confirm(AppLocalization["Are you sure you want to delete this API token?"], AppLocalization["Confirm delete"])) + { + if (tokenToDelete != null) + { + tokenToDelete.DeletedAt = DateTime.UtcNow; + Context.SaveChangesAsync(); + } + } + + } +} diff --git a/BLAZAM/Pages/Configure/Audit.razor b/BLAZAM/Pages/Configure/Audit.razor index e62dcb98a..8b10ccfde 100644 --- a/BLAZAM/Pages/Configure/Audit.razor +++ b/BLAZAM/Pages/Configure/Audit.razor @@ -177,6 +177,16 @@ + + + + + + + @AppLocalization["Webhooks"] + + + diff --git a/BLAZAM/Pages/Configure/Fields.razor b/BLAZAM/Pages/Configure/Fields.razor index ee92a9053..fc1705c51 100644 --- a/BLAZAM/Pages/Configure/Fields.razor +++ b/BLAZAM/Pages/Configure/Fields.razor @@ -88,15 +88,14 @@ async Task> RefreshData(GridState currentState) { LoadingData = true; - await InvokeAsync(StateHasChanged); if (Context != null) { ADFields = await Context.CustomActiveDirectoryFields.Where(x => x.DeletedAt == null).ToListAsync(); } - LoadingData = false; var data = new GridData(); data.Items = ADFields; data.TotalItems = ADFields.Count; + LoadingData = false; return data; } diff --git a/BLAZAM/Pages/Configure/Permissions.razor b/BLAZAM/Pages/Configure/Permissions.razor index bfb88e993..d4b6dec4c 100644 --- a/BLAZAM/Pages/Configure/Permissions.razor +++ b/BLAZAM/Pages/Configure/Permissions.razor @@ -8,7 +8,7 @@ - + @AppLocalization["Permissions"] diff --git a/BLAZAM/Pages/Configure/Settings.razor b/BLAZAM/Pages/Configure/Settings.razor index 8d31f69ed..8cb5c5a0a 100644 --- a/BLAZAM/Pages/Configure/Settings.razor +++ b/BLAZAM/Pages/Configure/Settings.razor @@ -7,6 +7,7 @@ @AppLocalization["Settings"] + @AppLocalization["Settings"] @{ diff --git a/BLAZAM/Pages/Configure/WebHookAuditContent.razor b/BLAZAM/Pages/Configure/WebHookAuditContent.razor new file mode 100644 index 000000000..4358f7c55 --- /dev/null +++ b/BLAZAM/Pages/Configure/WebHookAuditContent.razor @@ -0,0 +1,77 @@ + @inherits DatabaseComponentBase + + + + + + @context.Item.MessageGuid.ToString() + + + + + + + @context.Item.LastAttemptTimestamp.ToLocalTime() + + + + + + + + + + + + + @context.Item.EventTimestamp.ToLocalTime() + + + + + + + + + + + +@code { + private List _attempts = new(); + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + _ = GetAttempts(); + } + private async Task GetAttempts() + { + _attempts = await Context.WebHookAttempts.ToListAsync(); + await InvokeAsync(StateHasChanged); + } +} + diff --git a/BLAZAM/Pages/Configure/WebHooks.razor b/BLAZAM/Pages/Configure/WebHooks.razor new file mode 100644 index 000000000..12ab54bb1 --- /dev/null +++ b/BLAZAM/Pages/Configure/WebHooks.razor @@ -0,0 +1,27 @@ +@page "/webhooks" +@using BLAZAM.Gui.UI.Settings.WebHooks +@attribute [Authorize(Roles = UserRoles.SuperAdmin)] + +@inherits TabbedAppComponentBase + +@AppLocalization["Webhooks"] + + + + + + + + + + + + + + + + + +@code { + +} diff --git a/BLAZAM/Pages/Groups/ConfirmNewGroup.razor b/BLAZAM/Pages/Groups/ConfirmNewGroup.razor index cb1db960f..6ab8b18e7 100644 --- a/BLAZAM/Pages/Groups/ConfirmNewGroup.razor +++ b/BLAZAM/Pages/Groups/ConfirmNewGroup.razor @@ -33,20 +33,20 @@ createGroupJob.StopOnFailedStep = true; createGroupJob.ShowJobDetailsDialog(MessageService); var result = await Group.CommitChangesAsync(createGroupJob); - + disableCreateGroupButton = false; await InvokeAsync(StateHasChanged); if (result.FailedSteps.Count == 0) { + Group = (IADGroup)Directory.GetDirectoryEntryByDN(Group.DN); SnackBarService.Success("Group created"); + await AuditLogger.Group.Created(Group); + _ = OUNotificationService.PostAsync(Group, NotificationType.Create, CurrentUser.State); + await Confirmed.InvokeAsync(Group); } - await AuditLogger.Group.Created(Group); - _ = OUNotificationService.PostAsync(Group, NotificationType.Create,CurrentUser.State); - - var commitJob = Group.CommitChanges(); - await Confirmed.InvokeAsync(Group); + } } } \ No newline at end of file diff --git a/BLAZAM/Pages/Groups/CreateGroup.razor b/BLAZAM/Pages/Groups/CreateGroup.razor index c5e822137..e02cb7496 100644 --- a/BLAZAM/Pages/Groups/CreateGroup.razor +++ b/BLAZAM/Pages/Groups/CreateGroup.razor @@ -1,4 +1,4 @@ -@page "/groups/create" +@page "/create/group" @inherits TabbedAppComponentBase @attribute [Authorize] Create Group @@ -106,7 +106,6 @@ async void CreateNewGroup() { LoadingData = true; - await InvokeAsync(StateHasChanged); newGroup = parentOU?.CreateGroup(newGroupName); @@ -116,6 +115,5 @@ LoadingData = true; - await InvokeAsync(StateHasChanged); } } diff --git a/BLAZAM/Pages/Login.razor b/BLAZAM/Pages/Login.razor index 6c38d2271..1afcfcb5e 100644 --- a/BLAZAM/Pages/Login.razor +++ b/BLAZAM/Pages/Login.razor @@ -8,6 +8,7 @@ { } +@AppLocalization["Login"] diff --git a/BLAZAM/Pages/OU/ConfirmNewOU.razor b/BLAZAM/Pages/OU/ConfirmNewOU.razor index e004b666f..169c6e057 100644 --- a/BLAZAM/Pages/OU/ConfirmNewOU.razor +++ b/BLAZAM/Pages/OU/ConfirmNewOU.razor @@ -13,9 +13,9 @@ } @code { -#nullable disable warnings + #nullable disable warnings + - [Parameter] public EventCallback Confirmed { get; set; } @@ -27,12 +27,16 @@ { if (await MessageService.Confirm("Are you sure you want to create this OU?", "Create OU")) { - await OU.CommitChangesAsync(); - SnackBarService.Success("OU created"); - await AuditLogger.OU.Created(OU); - _ = OUNotificationService.PostAsync(OU, NotificationType.Create, CurrentUser.State); - await Confirmed.InvokeAsync(OU); - Nav.NavigateTo("/ou/create",true); + var results = await OU.CommitChangesAsync(); + if (results.FailedSteps.Count == 0) + { + OU = (IADOrganizationalUnit)Directory.GetDirectoryEntryByDN(OU.DN); + SnackBarService.Success("OU created"); + await AuditLogger.OU.Created(OU); + _ = OUNotificationService.PostAsync(OU, NotificationType.Create, CurrentUser.State); + await Confirmed.InvokeAsync(OU); + Nav.NavigateTo("/ou/create", true); + } } } } \ No newline at end of file diff --git a/BLAZAM/Pages/OU/CreateOU.razor b/BLAZAM/Pages/OU/CreateOU.razor index 623bacf15..a4cb89237 100644 --- a/BLAZAM/Pages/OU/CreateOU.razor +++ b/BLAZAM/Pages/OU/CreateOU.razor @@ -1,4 +1,4 @@ -@page "/ou/create" +@page "/create/ou" @inherits TabbedAppComponentBase @attribute [Authorize] @AppLocalization["Create OU"] @@ -94,7 +94,6 @@ LoadingData = false; - await InvokeAsync(StateHasChanged); } } diff --git a/BLAZAM/Pages/Search.razor b/BLAZAM/Pages/Search.razor index 2826247f0..01409c7f5 100644 --- a/BLAZAM/Pages/Search.razor +++ b/BLAZAM/Pages/Search.razor @@ -197,7 +197,6 @@ Searcher.Results.Clear(); LoadingData = false; - await InvokeAsync(StateHasChanged); diff --git a/BLAZAM/Pages/Users/ConfirmNewUser.razor b/BLAZAM/Pages/Users/ConfirmNewUser.razor index f4e980a89..9679c155a 100644 --- a/BLAZAM/Pages/Users/ConfirmNewUser.razor +++ b/BLAZAM/Pages/Users/ConfirmNewUser.razor @@ -86,12 +86,12 @@ InDirectoryTemplate(ActiveDirectoryFields.PhysicalDeliveryOffice) } @if (InDirectoryTemplate(ActiveDirectoryFields.HomePhone) || - InDirectoryTemplate(ActiveDirectoryFields.StreetAddress) || - InDirectoryTemplate(ActiveDirectoryFields.POBox) || - InDirectoryTemplate(ActiveDirectoryFields.City) || - InDirectoryTemplate(ActiveDirectoryFields.State) || - InDirectoryTemplate(ActiveDirectoryFields.PostalCode) - ) + InDirectoryTemplate(ActiveDirectoryFields.StreetAddress) || + InDirectoryTemplate(ActiveDirectoryFields.POBox) || + InDirectoryTemplate(ActiveDirectoryFields.City) || + InDirectoryTemplate(ActiveDirectoryFields.State) || + InDirectoryTemplate(ActiveDirectoryFields.PostalCode) + ) { @@ -146,10 +146,10 @@ InDirectoryTemplate(ActiveDirectoryFields.PhysicalDeliveryOffice) Disabled=true /> } @if (InDirectoryTemplate(ActiveDirectoryFields.HomeDirectory) || - InDirectoryTemplate(ActiveDirectoryFields.HomeDrive) || - InDirectoryTemplate(ActiveDirectoryFields.ScriptPath) || - InDirectoryTemplate(ActiveDirectoryFields.ProfilePath) - ) + InDirectoryTemplate(ActiveDirectoryFields.HomeDrive) || + InDirectoryTemplate(ActiveDirectoryFields.ScriptPath) || + InDirectoryTemplate(ActiveDirectoryFields.ProfilePath) + ) { @if (InDirectoryTemplate(ActiveDirectoryFields.HomeDirectory)) @@ -276,11 +276,12 @@ else _username = User.SamAccountName; _userPassword = User.NewPassword; var result = await User.CommitChangesAsync(createUserJob); - disableCreateUserButton = false; await InvokeAsync(StateHasChanged); if (result.FailedSteps.Count == 0) { + User = (IADUser)Directory.GetDirectoryEntryByDN(User.DN); + SnackBarService.Success("User has been created"); confirmed = true; await Confirmed.InvokeAsync(); diff --git a/BLAZAM/Pages/Users/CreateUser.razor b/BLAZAM/Pages/Users/CreateUser.razor index e7e962ec4..438e3420d 100644 --- a/BLAZAM/Pages/Users/CreateUser.razor +++ b/BLAZAM/Pages/Users/CreateUser.razor @@ -1,4 +1,4 @@ -@page "/users/create" +@page "/create/user" @inherits TemplateComponent @attribute [Authorize(Roles = UserRoles.CreateUsers)] @@ -356,34 +356,8 @@ newUser?.StagePasswordChange(customConfirmPassword.ToSecureString()); { LoadingData = true; await InvokeAsync(StateHasChanged); - var ou = Directory.OUs.FindOuByString(SelectedTemplate.EffectiveParentOU).FirstOrDefault(); - if (ou == null) throw new ApplicationException("OU could not be found for new user"); - newUser = ou.CreateUser(SelectedTemplate.GenerateDisplayName(newUserName)); - - newUser.SamAccountName = SelectedTemplate.GenerateUsername(newUserName); - newUser.DisplayName = SelectedTemplate.GenerateDisplayName(newUserName); - //newUser.SetPassword(SelectedTemplate.GeneratePassword().ToSecureString(),false); - //newUser.CanonicalName = SelectedTemplate.GenerateDisplayName(newUserName); - newUser.StagePasswordChange(SelectedTemplate.GeneratePassword(newUserName).ToSecureString()); - if (SelectedTemplate.EffectiveRequirePasswordChange == true) - newUser.StageRequirePasswordChange(true); - if (!newUserName.GivenName.IsNullOrEmpty()) - newUser.GivenName = newUserName.GivenName; - if (!newUserName.MiddleName.IsNullOrEmpty()) - newUser.MiddleName = newUserName.MiddleName; - if (!newUserName.Surname.IsNullOrEmpty()) - newUser.Surname = newUserName.Surname; - - - - SelectedTemplate.EffectiveAssignedGroupSids.ForEach(sid => - { - var group = Directory.Groups.FindGroupBySID(sid.GroupSid); - if (group != null) - newUser.AssignTo(group); - - }); - if (IsAdmin || SelectedTemplate.HasEmptyFields()) + newUser = SelectedTemplate.GenerateTemplateUser(newUserName,Directory); + if (IsAdmin || SelectedTemplate.HasEditableFields()) { SelectedStep = 2; } @@ -405,7 +379,7 @@ newUser?.StagePasswordChange(customConfirmPassword.ToSecureString()); await InvokeAsync(StateHasChanged); } - + async Task SetTemplate(DirectoryTemplate selectedTemplate) { SelectedTemplate = selectedTemplate; diff --git a/BLAZAM/Pages/View.razor b/BLAZAM/Pages/View.razor index 869d92b52..c0ceaf837 100644 --- a/BLAZAM/Pages/View.razor +++ b/BLAZAM/Pages/View.razor @@ -174,14 +174,12 @@ results.Clear(); LoadingData = true; - await InvokeAsync(StateHasChanged); if (!SearchTermParameter.IsNullOrEmpty() && SearchTermParameter?.Length > 0) await InvokeSearch(); else Searcher.Results.Clear(); LoadingData = false; - await InvokeAsync(StateHasChanged); diff --git a/BLAZAM/Pages/_Layout.cshtml b/BLAZAM/Pages/_Layout.cshtml index 4ad942ecf..64304cfaa 100644 --- a/BLAZAM/Pages/_Layout.cshtml +++ b/BLAZAM/Pages/_Layout.cshtml @@ -41,8 +41,9 @@ } else { - An unhandled exception has occurred.See browser dev tools for details. + An unhandled exception has occurred. See browser dev tools for details. + } Reload 🗙 @@ -112,4 +113,4 @@
You are not authorized to access this resource.