diff --git a/CDP-LTS-80.sln b/CDP-LTS-80.sln index 768a21a..3426627 100644 --- a/CDP-LTS-80.sln +++ b/CDP-LTS-80.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.9.34622.214 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CDP-LTS-80", "CDP-LTS-80\CDP-LTS-80.csproj", "{C9E49B28-1B40-4C62-AF86-C674677D3AB9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CDP-LTS-80", "CDP-LTS-80\CDP-LTS-80.csproj", "{C9E49B28-1B40-4C62-AF86-C674677D3AB9}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/CDP-LTS-80/AuditDB.cs b/CDP-LTS-80/Audit/AuditDB.cs similarity index 100% rename from CDP-LTS-80/AuditDB.cs rename to CDP-LTS-80/Audit/AuditDB.cs diff --git a/CDP-LTS-80/AuditDocument.cs b/CDP-LTS-80/Audit/AuditDocument.cs similarity index 100% rename from CDP-LTS-80/AuditDocument.cs rename to CDP-LTS-80/Audit/AuditDocument.cs diff --git a/CDP-LTS-80/Audit/AuditFunctions.cs b/CDP-LTS-80/Audit/AuditFunctions.cs new file mode 100644 index 0000000..9eae50d --- /dev/null +++ b/CDP-LTS-80/Audit/AuditFunctions.cs @@ -0,0 +1,263 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace CDP +{ + public class AuditFunctions + { + private readonly ILogger _logger; + public static string FileAuditContainer = "FileAudits"; + public static string UserAuditContainer = "UserAudits"; + public static string GroupAuditContainer = "GroupAudits"; + public static string TenantAuditContainer = "TenantAudits"; + public AuditFunctions(ILogger logger) + { + _logger = logger; + } + + [Function("GetAuditLogForFile")] + public async Task GetAuditLogForFile([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) + { + _logger.LogInformation("GetAuditLogForFile invoked"); + + // Convert the JSON payload to a string + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + if (string.IsNullOrEmpty(requestBody)) + return new BadRequestObjectResult(new { error = true, message = "The body is empty" }); + + _logger.LogInformation(requestBody); + + GetAuditLogForFileDto dto = JsonConvert.DeserializeObject(requestBody); + if (dto == null) + return new BadRequestObjectResult(new { error = true, message = "Parse error." }); + + List ad = await AuditDB.GetAuditRecordsBetweenDates(dto.FileId, DateTime.MinValue, DateTime.MaxValue, CDPLite.FileAuditContainer); + + return new OkObjectResult(ad); + } + + [Function("GetAuditLogForUser")] + public async Task GetAuditLogForUser([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) + { + _logger.LogInformation("GetAuditLogForUser invoked"); + + // Convert the JSON payload to a string + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + if (string.IsNullOrEmpty(requestBody)) + return new BadRequestObjectResult(new { error = true, message = "The body is empty" }); + + _logger.LogInformation(requestBody); + + GetAuditLogForUserDto dto = JsonConvert.DeserializeObject(requestBody); + if (dto == null) + return new BadRequestObjectResult(new { error = true, message = "Parse error." }); + + string userId = Helpers.HashAndShortenText(dto.Email.ToLower()); + List ad = await AuditDB.GetAuditRecordsBetweenDates(userId, DateTime.MinValue, DateTime.MaxValue, UserAuditContainer); + return new OkObjectResult(ad); + } + + [Function("GetAuditLogForGroup")] + public async Task GetAuditLogForGroup([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) + { + _logger.LogInformation("GetAuditLogForGroup invoked"); + + // Convert the JSON payload to a string + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + if (string.IsNullOrEmpty(requestBody)) + return new BadRequestObjectResult(new { error = true, message = "The body is empty" }); + + _logger.LogInformation(requestBody); + + GetAuditLogForGroupDto dto = JsonConvert.DeserializeObject(requestBody); + if (dto == null) + return new BadRequestObjectResult(new { error = true, message = "Parse error." }); + + List ad = await AuditDB.GetAuditRecordsBetweenDates(dto.GroupId, DateTime.MinValue, DateTime.MaxValue, GroupAuditContainer); + return new OkObjectResult(ad); + } + + [Function("GetAuditLogForTenant")] + public async Task GetAuditLogForTenant([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) + { + _logger.LogInformation("GetAuditLogForGroup invoked"); + + // Convert the JSON payload to a string + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + if (string.IsNullOrEmpty(requestBody)) + return new BadRequestObjectResult(new { error = true, message = "The body is empty" }); + + _logger.LogInformation(requestBody); + + GetAuditLogForTenantDto dto = JsonConvert.DeserializeObject(requestBody); + if (dto == null) + return new BadRequestObjectResult(new { error = true, message = "Parse error." }); + + List ad = await AuditDB.GetAuditRecordsBetweenDates(dto.AppKey, DateTime.MinValue, DateTime.MaxValue, TenantAuditContainer); + return new OkObjectResult(ad); + } + + [Function("AddAccessViolation")] + public async Task AddAccessViolation([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) + { + _logger.LogInformation("AddAccessViolation invoked"); + + // Convert the JSON payload to a string + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + if (string.IsNullOrEmpty(requestBody)) + return false; + + _logger.LogInformation(requestBody); + + AddAccessViolationDto dto = JsonConvert.DeserializeObject(requestBody); + if (dto == null) + return false; + + string message = string.Format($"Access violation recorded for file {dto.FileName}"); + string action = "Access Violation"; + await AddAudits(dto.AppKey, dto.FileId, dto.FileName, "", "", action, message); + return true; + } + + public static async Task AddAuditsEvent(string appKey, string fileId, string fileName, string userId, string groupid, string action, string message) + { + using (var mt = new MethodTimer("AddAuditsEventMessage")) + { + if (string.IsNullOrEmpty(appKey) || string.IsNullOrEmpty(fileId) || string.IsNullOrEmpty(action) || string.IsNullOrEmpty(message)) + return ""; + AuditEventMetadata auditEvent = new AuditEventMetadata + { + FileId = fileId, + FileName = fileName, + UserId = userId, + GroupId = groupid, + Action = action, + Message = message + }; + string jobId = Guid.NewGuid().ToString(); + string jobMeta = JsonConvert.SerializeObject(auditEvent); + Job job = new Job { AppKey = appKey, EventType = JobType.AddAudits, Id = jobId, JobMetadata = jobMeta }; + await MetaProcessor.PublishJob(job); + return jobId; + } + } + + + public static async Task AddAudits(string appKey, string fileId, string fileName, string userId, string groupid, string action, string message) + { + if (string.IsNullOrEmpty(appKey) || string.IsNullOrEmpty(fileId) || string.IsNullOrEmpty(action) || string.IsNullOrEmpty(message)) + { + Console.WriteLine(string.Format("something weird? appKey, fileId, action, message: {0} {1} {2} {3}", appKey, fileId, action, message)); + return; + } + + + AuditRecord faRec = new FileAuditRecord() + { + AppKey = appKey, + FileId = fileId, + FileName = fileName, + UserId = userId, + GroupId = groupid, + Action = action, + Message = message, + EventTime = DateTime.UtcNow, + }; + Console.WriteLine("Adding File Audit Record"); + await AuditDB.AppendRecord(faRec.id, faRec, FileAuditContainer); + + AuditRecord faRecTenant = new TenantAuditRecord() + { + AppKey = appKey, + FileId = fileId, + FileName = fileName, + UserId = userId, + GroupId = groupid, + Action = action, + Message = message, + EventTime = DateTime.UtcNow, + }; + + await AuditDB.AppendRecord(faRecTenant.id, faRecTenant, TenantAuditContainer); + + if (!string.IsNullOrEmpty(groupid)) + { + AuditRecord faRecGroup = new GroupAuditRecord() + { + AppKey = appKey, + FileId = fileId, + FileName = fileName, + UserId = userId, + GroupId = groupid, + Action = action, + Message = message, + EventTime = DateTime.UtcNow, + }; + + await AuditDB.AppendRecord(faRecGroup.id, faRecGroup, GroupAuditContainer); + } + + AuditRecord faRecUser = new UserAuditRecord() + { + AppKey = appKey, + FileId = fileId, + FileName = fileName, + UserId = userId, + GroupId = groupid, + Action = action, + Message = message, + EventTime = DateTime.UtcNow, + }; + + await AuditDB.AppendRecord(faRecUser.id, faRecUser, UserAuditContainer); + + } + /// + /// Adds the audit record on a background thread. + /// + private static async Task AddFileAudit(AuditRecord far) + { + await AuditDB.AppendRecord(far.id, far, FileAuditContainer); + + } + + private static async Task AddUserAudit(AuditRecord far) + { + await AuditDB.AppendRecord(far.id, far, UserAuditContainer); + } + + private static async Task AddTenantAudit(AuditRecord far) + { + await Task.Run(async () => + { + try + { + await AuditDB.AppendRecord(far.id, far, TenantAuditContainer); + } + catch (Exception e) + { + } + }); + + } + + private static async Task AddGroupAudit(AuditRecord far) + { + await Task.Run(async () => + { + try + { + await AuditDB.AppendRecord(far.id, far, GroupAuditContainer); + } + catch (Exception e) + { + } + }); + + } + } +} diff --git a/CDP-LTS-80/CDP-LTS-80.csproj b/CDP-LTS-80/CDP-LTS-80.csproj index 9dfcfa0..f1615fe 100644 --- a/CDP-LTS-80/CDP-LTS-80.csproj +++ b/CDP-LTS-80/CDP-LTS-80.csproj @@ -6,19 +6,25 @@ enable enable CDP + efb0fbf7-afd0-468d-9e4c-605bbab91059 + + + + + diff --git a/CDP-LTS-80/CDPBlobStorage.cs b/CDP-LTS-80/CDPCore/CDPBlobStorage.cs similarity index 100% rename from CDP-LTS-80/CDPBlobStorage.cs rename to CDP-LTS-80/CDPCore/CDPBlobStorage.cs diff --git a/CDP-LTS-80/CDPDB.cs b/CDP-LTS-80/CDPCore/CDPDB.cs similarity index 100% rename from CDP-LTS-80/CDPDB.cs rename to CDP-LTS-80/CDPCore/CDPDB.cs diff --git a/CDP-LTS-80/CDPCore/CDPLite.cs b/CDP-LTS-80/CDPCore/CDPLite.cs new file mode 100644 index 0000000..f5391ab --- /dev/null +++ b/CDP-LTS-80/CDPCore/CDPLite.cs @@ -0,0 +1,344 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.Cosmos; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CDP +{ + public class CDPLite + { + private readonly ILogger _logger; + public static string FileAuditContainer = "FileAudits"; + public static string UserAuditContainer = "UserAudits"; + public static string GroupAuditContainer = "GroupAudits"; + public static string TenantAuditContainer = "TenantAudits"; + + public CDPLite(ILogger log) + { + _logger = log; + } + + /*internal async Task AddFilesBatchedInternal(AddFileBatchedDto dto) + { + string userId = Helpers.HashAndShortenText(dto.Email.ToLower()); + string jobId = Guid.NewGuid().ToString(); + List vaultEvents = new List(); + //List auditEvents = new List(); + List fileRecords = new List(); + for (int i = 0; i < dto.Count; i++) + { + string fileId = Guid.NewGuid().ToString(); + string fileName = dto.FileNames[i]; + string aesKey = Helpers.GenerateAES256KeyToBase64(); + //string message = string.Format($"{dto.Email} protected {fileName} file having {fileId} id."); + KeyVaultEvent vaultEvent = new KeyVaultEvent { AESKey = aesKey, FileId = fileId }; + //AuditEventMetadata auditEvent = new AuditEventMetadata { Action = "Addded", FileId = fileId, FileName = dto.FileNames[i], Message = message, UserId = userId }; + //auditEvents.Add(auditEvent); + vaultEvents.Add(vaultEvent); + + AccessPolicy ac = new AccessPolicy() + { + Access = "Owner", + Key = aesKey, + Email = dto.Email + }; + FileRecord fr = await CDPDB.UpsertFile(dto.AppKey, fileId, fileName, userId, "", ac); + fileRecords.Add(fr); + } + List jobs = new List(); + Job vaultJob = await AddKeyVaultBatchedEvent(vaultEvents, dto.AppKey); + //Job auditJob = await AddAuditsBatchedEvent(auditEvents, dto.AppKey); + //jobs.Add(auditJob); + jobs.Add(vaultJob); + await MetaProcessor.PublishBatchJobs(jobs); + return new OkObjectResult(fileRecords); + + }*/ + + + internal async Task AddFileInternal(AddFileDto dto, bool useKeyVaultEvent = false) + { + string fileId = Guid.NewGuid().ToString(); + + string userId = Helpers.HashAndShortenText(dto.Email.ToLower()); + string fileName = dto.FileName; + string aesKey = Helpers.GenerateAES256KeyToBase64(); + + // the KeyVault is slow for some reason and while it's dangerous to return a key + // before we're sure it got added to the database...I'm going to do it anyway. + if (useKeyVaultEvent) + { + await AddKeyVaultEvent(fileId, aesKey, dto.AppKey); + } + else + { + await Task.Run(async () => + { + try + { + KeyVaultService kvs = new KeyVaultService(Constants.KeyVaultURI); + await kvs.SetSecretAsync(fileId, aesKey); + } + catch (Exception e) + { + Console.WriteLine(e); + } + }); + } + + // when you add a file, you have rights to manage access to it. + AccessPolicy ac = new AccessPolicy() + { + Access = "Owner", + Key = aesKey, + Email = dto.Email + }; + + // since we're generating a new file id, a new entry will always be created. + FileRecord fr = await CDPDB.UpsertFile(dto.AppKey, fileId, fileName, userId, "", ac); + + string message = string.Format($"{dto.Email} protected {fileName} file having {fileId} id."); + string action = "Added"; + + await AuditFunctions.AddAuditsEvent(dto.AppKey, fileId, fileName, userId, dto.GroupId, action, message); //commenting for speed test + return new OkObjectResult(fr); + } + + internal static async Task AddFileUserInternal(AddFileUserDto dto) + { + // check to see if the email has the power to add a user + string userId = Helpers.HashAndShortenText(dto.Email.ToLower()); + + FileRecord fr = await CDPDB.GetFile(dto.AppKey, dto.FileId, userId); + if (fr == null) + { + string message = string.Format($"{dto.Email} attempted to add/change access policy for {dto.EmailToAdd} on {dto.FileName} file having {dto.FileId} id, but didn't have ANY access"); + Console.WriteLine(message); + string action = "Policy change failed"; + await AuditFunctions.AddAuditsEvent(dto.AppKey, dto.FileId, dto.FileName, userId, "", action, message); + + return new BadRequestObjectResult(new { error = true, message = "File not found for user " + dto.Email }); + } + + if ((!fr.Policy.CheckAccess("Manage")) && (!fr.Policy.CheckAccess("Owner"))) + { + string message = string.Format($"{dto.Email} attempted to add/change access policy for {dto.EmailToAdd} on {dto.FileName} file having {dto.FileId} id, but didn't have manage access"); + Console.WriteLine(message); + string action = "Policy change failed"; + await AuditFunctions.AddAuditsEvent(dto.AppKey, dto.FileId, dto.FileName, userId, "", action, message); + return new BadRequestObjectResult(new { error = true, message = $"{dto.Email} doesn't have the rights to add a user." }); + } + + string fileId = dto.FileId; + string fileName = dto.FileName; + string userIdToAdd = ""; + + if (dto.EmailToAdd != "") + { + userIdToAdd = Helpers.HashAndShortenText(dto.EmailToAdd.ToLower()); + } + else if (dto.Group != null) + { + userIdToAdd = dto.GroupId; + } + else if (dto.Group != null) + { + userIdToAdd = dto.GroupId; + } + + AccessPolicy ac = new AccessPolicy() + { + Access = dto.Policy, + Email = dto.EmailToAdd.ToLower(), + Group = dto.Group, + GroupId = dto.GroupId, + Key = "" + }; + + fr = await CDPDB.UpsertFile(dto.AppKey, fileId, fileName, userIdToAdd, "", ac); + + if (dto.EmailToAdd != "") + { + string message = string.Format($"{dto.Email} added/changed the access policy for User : {dto.EmailToAdd} to {dto.Policy} on {fileName} file having {fileId} id"); + string action = "Policy change"; + await AuditFunctions.AddAuditsEvent(dto.AppKey, fileId, fileName, userId, "", action, message); + } + + if (dto.Group != null) + { + string message = string.Format($"{dto.Email} added/changed the access policy for Group : {dto.Group} to {dto.Policy} on {fileName} file having {fileId} id"); + string action = "Policy change"; + await AuditFunctions.AddAuditsEvent(dto.AppKey, fileId, fileName, "", dto.Group.id, action, message); + } + return new OkObjectResult(fr); + } + + #region CDP File Functions + + [Function("AddFile")] + public async Task AddFile([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) + { + _logger.LogInformation("AddFile invoked"); + + // Convert the JSON payload to a string + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + AddFileDto dto = JsonConvert.DeserializeObject(requestBody); + if (dto == null) + return new BadRequestObjectResult(new { error = true, message = "Parse error." }); + + return await AddFileInternal(dto, true); + } + + [Function("AddFileUser")] + public async Task AddFileUser([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) + { + _logger.LogInformation("AddFileUser invoked"); + + // Convert the JSON payload to a string + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + AddFileUserDto dto = JsonConvert.DeserializeObject(requestBody); + if (dto == null) + return new BadRequestObjectResult(new { error = true, message = "Parse error." }); + + return await AddFileUserInternal(dto); + } + + [Function("GetFileForUser")] + public async Task GetFileForUser([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) + { + _logger.LogInformation("GetFile invoked"); + + // Convert the JSON payload to a string + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + AddFileUserDto dto = JsonConvert.DeserializeObject(requestBody); + if (dto == null) + return new BadRequestObjectResult(new { error = true, message = "Parse error." }); + + // check to see if the email has the power to add a user + // string a = Helpers.HashAndShortenText(dto.Email.ToLower()); + // string b = Helpers.HashToHex(dto.Email.ToLower()); + // string c = Helpers.ConvertShortHashToHex(a); + + string userId = Helpers.HashAndShortenText(dto.Email.ToLower()); + // userId = "user-" + Helpers.HashToHex(dto.Email.ToLower()); + + FileRecord fr = await CDPDB.GetFile(dto.AppKey, dto.FileId, userId); + if (fr == null) + { + string AuditMessage = string.Format($"File not found for user {dto.Email} having fileId {dto.FileId}"); + string AuditAction = "Decrypt failed"; + await AuditFunctions.AddAuditsEvent(dto.AppKey, dto.FileId, dto.FileName, userId, "", AuditAction, AuditMessage); + return new BadRequestObjectResult(new { error = true, message = "File not found for user " + dto.Email }); + } + + if (fr.Policy.CheckAccess("None")) + { + string AuditMessage = string.Format($"{dto.Email} don't have the rights to decrypt {fr.FileName} file having {dto.FileId} id"); + string AuditAction = "Decrypt failed"; + await AuditFunctions.AddAuditsEvent(dto.AppKey, dto.FileId, fr.FileName, userId, "", AuditAction, AuditMessage); + return new BadRequestObjectResult(new { error = true, message = "Access is denied for user " + dto.Email }); + } + + KeyVaultService kvs = new KeyVaultService(Constants.KeyVaultURI); + fr.Policy.Key = await kvs.GetSecretAsync(fr.FileId); + + string message = string.Format($"{dto.Email} decrypted {fr.FileName} file having {dto.FileId} id"); + string action = "Decrypted"; + await AuditFunctions.AddAuditsEvent(dto.AppKey, dto.FileId, dto.FileName, userId, "", action, message); + + return new OkObjectResult(fr); + } + + [Function("GetPoliciesForFile")] + public async Task GetPoliciesForFile([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) + { + _logger.LogInformation("GetFile invoked"); + + // Convert the JSON payload to a string + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + GetPoliciesForFileDto dto = JsonConvert.DeserializeObject(requestBody); + if (dto == null) + return new BadRequestObjectResult(new { error = true, message = "Parse error." }); + + List fr = await CDPDB.GetPoliciesForFile(dto.AppKey, dto.FileId); + if (fr == null) + { + return new BadRequestObjectResult(new { error = true, message = "File not found " + dto.FileId }); + } + KeyVaultService kvs = new KeyVaultService(Constants.KeyVaultURI); + string aesKey = await kvs.GetSecretAsync(dto.FileId); + foreach (var f in fr) + { + f.Policy.Key = aesKey; + } + + return new OkObjectResult(fr); + } + + [Function("DeleteRegisteredUserPolicies")] + public async Task DeleteRegisteredUserPolicies([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) + { + _logger.LogInformation("Deleting Registered User invoked"); + + // Convert the JSON payload to a string + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + DeleteRegisteredUserDto deleteContactdto = JsonConvert.DeserializeObject(requestBody); + if (deleteContactdto == null) + return new BadRequestObjectResult(new { error = true, message = "Parse error." }); + + string userId = Helpers.HashAndShortenText(deleteContactdto.UserEmail.ToLower()); + await CDPDB.revokeRegisteredUserPolicies(deleteContactdto.AppKey, deleteContactdto.UserEmail.ToLower(), deleteContactdto.ContactEmail.ToLower(), deleteContactdto.AdminEmail.ToLower()); + + return new OkObjectResult(true); + } + + #endregion + + #region Background Processing Support + + public static async Task AddAuditsBatchedEvent(List auditEvents, string appKey) + { + using (var mt = new MethodTimer("AddAuditsBatchedEvent")) + { + string jobMeta = JsonConvert.SerializeObject(auditEvents); + string jobId = Guid.NewGuid().ToString(); + Job job = new Job { AppKey = appKey, EventType = JobType.AddAuditsBatch, Id = jobId, JobMetadata = jobMeta }; + return job; + } + } + + public static async Task AddKeyVaultBatchedEvent(List vaultEvents, string appKey) + { + using (var mt = new MethodTimer("AddKeyVaultBatchedEvent")) + { + string jobMeta = JsonConvert.SerializeObject(vaultEvents); + string jobId = Guid.NewGuid().ToString(); + Job job = new Job { AppKey = appKey, EventType = JobType.KeyVaultInsertionBatch, Id = jobId, JobMetadata = jobMeta }; + //await MetaProcessor.PublishJob(job); + return job; + } + } + + public static async Task AddKeyVaultEvent(string fileId, string aesKey, string appKey) + { + using (var mt = new MethodTimer("AddKeyVaultEvent")) + { + KeyVaultEvent vaultEvent = new KeyVaultEvent { AESKey = aesKey, FileId = fileId }; + string jobMeta = JsonConvert.SerializeObject(vaultEvent); + string jobId = Guid.NewGuid().ToString(); + Job keyVaultJob = new Job { AppKey = appKey, EventType = JobType.KeyVaultInsertion, Id = jobId, JobMetadata = jobMeta }; + await MetaProcessor.PublishJob(keyVaultJob); + return jobId; + } + } + + #endregion + } +} diff --git a/CDP-LTS-80/FileRevision.cs b/CDP-LTS-80/CDPCore/FileRevision.cs similarity index 59% rename from CDP-LTS-80/FileRevision.cs rename to CDP-LTS-80/CDPCore/FileRevision.cs index 0477830..dabe6e2 100644 --- a/CDP-LTS-80/FileRevision.cs +++ b/CDP-LTS-80/CDPCore/FileRevision.cs @@ -2,8 +2,11 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.Cosmos; using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; +using Newtonsoft.Json; using System.Net; +using FromBodyAttribute = Microsoft.Azure.Functions.Worker.Http.FromBodyAttribute; namespace CDP { @@ -29,6 +32,15 @@ namespace CDP return new CosmosClient(uri, authKey); } + public static async Task GetFileRevisionInternal(string appKey, string fileId) + { + Container container = cosmosClient.GetContainer(DatabaseName, ContainerName); + string id = Helpers.HashAndShortenText(appKey + fileId); + PartitionKey key = new PartitionKeyBuilder().Add(appKey).Add(fileId).Build(); + var item = await container.ReadItemAsync(id, key); + return item.Resource; + } + static internal async Task AddRevisionInternal(AddRevisionDocumentDto doc, ILogger _logger) { try @@ -60,6 +72,21 @@ namespace CDP } } + public async static Task AddMailBodyRevision(RevisionDocument document, CompoundDocument compoundDocument, string AppKey, string FileId) + { + Container container = cosmosClient.GetContainer(DatabaseName, ContainerName); + List documents = new List { document }; + RevisionEntry entry = new RevisionEntry + { + AppKey = AppKey, + FileId = FileId, + FileRevisions = documents, + CompoundDocument = compoundDocument + }; + ItemResponse item = await container.CreateItemAsync(entry); + return item.Resource.id; + } + private async static Task AddOrUpdateRevision(Container container, string id, PartitionKey partitionKey, RevisionDocument document, string appKey, string fileId) { try @@ -103,6 +130,54 @@ namespace CDP } + #region Versioning Functions + + [Function("AddRevisionDocument")] + public async Task AddRevisionDocument([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) + { + _logger.LogInformation("C# HTTP trigger function processed a request for update revision"); + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + AddRevisionDocumentDto doc; + try + { + doc = JsonConvert.DeserializeObject(requestBody); + _logger.LogInformation(doc.RevisionId + doc.AppKey + doc.FileId); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return new BadRequestObjectResult("Invalid request format"); + } + + return await AddRevisionInternal(doc, _logger); + } + + [Function("GetFileRevisions")] + public async Task GetFileRevisions([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req, [FromBody] GetRevisionsDto reqDto) + { + _logger.LogInformation("C# HTTP trigger function processed a request for update revision", JsonConvert.SerializeObject(reqDto)); + + try + { + + var item = await GetFileRevisionInternal(reqDto.AppKey, reqDto.FileId); + if (item.CompoundDocument != null) + { + return new OkObjectResult(item); + } + List revisions = item.FileRevisions; + //_logger.LogInformation("docs: {0}", JsonConvert.SerializeObject(revisions)); + + return new OkObjectResult(revisions); + } + catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) + { + return new StatusCodeResult(404); + } + } + + #endregion + } public class GetRevisionsDto @@ -128,6 +203,7 @@ namespace CDP public required string Comments { get; set; } } + public class RevisionEntry { public string id @@ -141,5 +217,13 @@ namespace CDP public required string AppKey { get; set; } public required string FileId { get; set; } public List FileRevisions { get; set; } + public CompoundDocument? CompoundDocument { get; set; } + } + public class CompoundDocument + { + public required string Id { get; set; } + public required string AppKey { get; set; } + public required string Contents { get; set; } //can be MailRecord below + public required string DocumentType { get; set; } } } diff --git a/CDP-LTS-80/MailProcessor.cs b/CDP-LTS-80/CDPCore/MailProcessor.cs similarity index 100% rename from CDP-LTS-80/MailProcessor.cs rename to CDP-LTS-80/CDPCore/MailProcessor.cs diff --git a/CDP-LTS-80/CDPCore/SignalRFunctions.cs b/CDP-LTS-80/CDPCore/SignalRFunctions.cs new file mode 100644 index 0000000..c1892be --- /dev/null +++ b/CDP-LTS-80/CDPCore/SignalRFunctions.cs @@ -0,0 +1,111 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System.Net; + +namespace CDP.CDPCore +{ + /*public class SignalRFunctions + { + private readonly ILogger _logger; + + private static readonly HttpClient HttpClient = new(); + private static string Etag = string.Empty; + private static int StarCount = 0; + + public SignalRFunctions(ILogger log) + { + _logger = log; + } + + [Function("index")] + public static HttpResponseData GetHomePage([HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequestData req) + { + var response = req.CreateResponse(HttpStatusCode.OK); + response.WriteString(File.ReadAllText("content/index.html")); + response.Headers.Add("Content-Type", "text/html"); + return response; + } + + [Function("negotiate")] + public static HttpResponseData Negotiate([HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequestData req, + [SignalRConnectionInfoInput(HubName = "cdp")] string connectionInfo) + { + var response = req.CreateResponse(HttpStatusCode.OK); + response.Headers.Add("Content-Type", "application/json"); + response.WriteString(connectionInfo); + return response; + } + + [Function(nameof(SendMessage))] + [SignalROutput(HubName = "cdp")] + public static async Task SendMessage([HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req) + { + string requestBody = new StreamReader(req.Body).ReadToEnd(); + SendMessagePayload pl = JsonConvert.DeserializeObject(requestBody); + if (pl == null) + return null; + + return await SendMessageInternal(pl); + + } + + public static async Task SendMessageInternal(SendMessagePayload pl) + { + if (pl == null) + return null; + + if (string.IsNullOrEmpty(pl.ConnectionId) && string.IsNullOrEmpty(pl.UserId)) + return null; + + try + { + Console.WriteLine($"Sending message to {pl.ConnectionId}"); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + + return new SignalRMessageAction(pl.Target) + { + Arguments = new[] { pl.Message }, + ConnectionId = pl.ConnectionId.Trim() + }; + } + + //[Function("broadcast")] + //[SignalROutput(HubName = "cdp")] + //public static async Task Broadcast([TimerTrigger("5 * * * * *] TimerInfo timerInfo) + //{ + // var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/repos/azure/azure-signalr"); + // request.Headers.UserAgent.ParseAdd("Serverless"); + // request.Headers.Add("If-None-Match", Etag); + // var response = await HttpClient.SendAsync(request); + // if (response.Headers.Contains("Etag")) + // { + // Etag = response.Headers.GetValues("Etag").First(); + // } + // if (response.StatusCode == HttpStatusCode.OK) + // { + // var result = await response.Content.ReadFromJsonAsync(); + // if (result != null) + // { + // StarCount = result.StarCount; + // } + // } + // return new SignalRMessageAction("newMessage", new object[] { $"Current star count of https://github.com/Azure/azure-signalr is: {StarCount}" }); + //} + }*/ + + public class SendMessagePayload + { + public string ConnectionId { get; set; } + public string UserId { get; set; } + public string Message { get; set; } + public string Target { get; set; } // this will default to 'newMessage' + } +} diff --git a/CDP-LTS-80/CDPLite.cs b/CDP-LTS-80/CDPLite.cs deleted file mode 100644 index 0634023..0000000 --- a/CDP-LTS-80/CDPLite.cs +++ /dev/null @@ -1,207 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.Cosmos; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CDP -{ - public class CDPLite - { - private readonly ILogger _logger; - private static string FileAuditContainer = "FileAudits"; - private static string UserAuditContainer = "UserAudits"; - private static string GroupAuditContainer = "GroupAudits"; - private static string TenantAuditContainer = "TenantAudits"; - - public CDPLite(ILogger log) - { - _logger = log; - } - - internal static async Task AddFileUserInternal(AddFileUserDto dto) - { - // check to see if the email has the power to add a user - string userId = Helpers.HashAndShortenText(dto.Email.ToLower()); - - FileRecord fr = await CDPDB.GetFile(dto.AppKey, dto.FileId, userId); - if (fr == null) - { - string message = string.Format($"{dto.Email} attempted to add/change access policy for {dto.EmailToAdd} on {dto.FileName} file having {dto.FileId} id, but didn't have ANY access"); - Console.WriteLine(message); - string action = "Policy change failed"; - await AddAudits(dto.AppKey, dto.FileId, dto.FileName, userId, "", action, message); - - return new BadRequestObjectResult(new { error = true, message = "File not found for user " + dto.Email }); - } - - if ((!fr.Policy.CheckAccess("Manage")) && (!fr.Policy.CheckAccess("Owner"))) - { - string message = string.Format($"{dto.Email} attempted to add/change access policy for {dto.EmailToAdd} on {dto.FileName} file having {dto.FileId} id, but didn't have manage access"); - Console.WriteLine(message); - string action = "Policy change failed"; - await AddAudits(dto.AppKey, dto.FileId, dto.FileName, userId, "", action, message); - return new BadRequestObjectResult(new { error = true, message = $"{dto.Email} doesn't have the rights to add a user." }); - } - - string fileId = dto.FileId; - string fileName = dto.FileName; - string userIdToAdd = ""; - - if (dto.EmailToAdd != "") - { - userIdToAdd = Helpers.HashAndShortenText(dto.EmailToAdd.ToLower()); - } - else if (dto.Group != null) - { - userIdToAdd = dto.GroupId; - } - else if (dto.Group != null) - { - userIdToAdd = dto.GroupId; - } - - AccessPolicy ac = new AccessPolicy() - { - Access = dto.Policy, - Email = dto.EmailToAdd.ToLower(), - Group = dto.Group, - GroupId = dto.GroupId, - Key = "" - }; - - fr = await CDPDB.UpsertFile(dto.AppKey, fileId, fileName, userIdToAdd, "", ac); - - if (dto.EmailToAdd != "") - { - string message = string.Format($"{dto.Email} added/changed the access policy for User : {dto.EmailToAdd} to {dto.Policy} on {fileName} file having {fileId} id"); - string action = "Policy change"; - await AddAudits(dto.AppKey, fileId, fileName, userId, "", action, message); - } - - if (dto.Group != null) - { - string message = string.Format($"{dto.Email} added/changed the access policy for Group : {dto.Group} to {dto.Policy} on {fileName} file having {fileId} id"); - string action = "Policy change"; - await AddAudits(dto.AppKey, fileId, fileName, "", dto.Group.id, action, message); - } - return new OkObjectResult(fr); - } - - public static async Task AddAudits(string appKey, string fileId, string fileName, string userId, string groupid, string action, string message) - { - if (string.IsNullOrEmpty(appKey) || string.IsNullOrEmpty(fileId) || string.IsNullOrEmpty(action) || string.IsNullOrEmpty(message)) - { - Console.WriteLine(string.Format("something weird? appKey, fileId, action, message: {0} {1} {2} {3}", appKey, fileId, action, message)); - return; - } - - - AuditRecord faRec = new FileAuditRecord() - { - AppKey = appKey, - FileId = fileId, - FileName = fileName, - UserId = userId, - GroupId = groupid, - Action = action, - Message = message, - EventTime = DateTime.UtcNow, - }; - Console.WriteLine("Adding File Audit Record"); - await AuditDB.AppendRecord(faRec.id, faRec, FileAuditContainer); - - AuditRecord faRecTenant = new TenantAuditRecord() - { - AppKey = appKey, - FileId = fileId, - FileName = fileName, - UserId = userId, - GroupId = groupid, - Action = action, - Message = message, - EventTime = DateTime.UtcNow, - }; - - await AuditDB.AppendRecord(faRecTenant.id, faRecTenant, TenantAuditContainer); - - if (!string.IsNullOrEmpty(groupid)) - { - AuditRecord faRecGroup = new GroupAuditRecord() - { - AppKey = appKey, - FileId = fileId, - FileName = fileName, - UserId = userId, - GroupId = groupid, - Action = action, - Message = message, - EventTime = DateTime.UtcNow, - }; - - await AuditDB.AppendRecord(faRecGroup.id, faRecGroup, GroupAuditContainer); - } - - AuditRecord faRecUser = new UserAuditRecord() - { - AppKey = appKey, - FileId = fileId, - FileName = fileName, - UserId = userId, - GroupId = groupid, - Action = action, - Message = message, - EventTime = DateTime.UtcNow, - }; - - await AuditDB.AppendRecord(faRecUser.id, faRecUser, UserAuditContainer); - - } - /// - /// Adds the audit record on a background thread. - /// - private static async Task AddFileAudit(AuditRecord far) - { - await AuditDB.AppendRecord(far.id, far, FileAuditContainer); - - } - - private static async Task AddUserAudit(AuditRecord far) - { - await AuditDB.AppendRecord(far.id, far, UserAuditContainer); - } - - private static async Task AddTenantAudit(AuditRecord far) - { - await Task.Run(async () => - { - try - { - await AuditDB.AppendRecord(far.id, far, TenantAuditContainer); - } - catch (Exception e) - { - } - }); - - } - - private static async Task AddGroupAudit(AuditRecord far) - { - await Task.Run(async () => - { - try - { - await AuditDB.AppendRecord(far.id, far, GroupAuditContainer); - } - catch (Exception e) - { - } - }); - - } - } -} diff --git a/CDP-LTS-80/Contacts/ContactFunctions.cs b/CDP-LTS-80/Contacts/ContactFunctions.cs new file mode 100644 index 0000000..15c9c2a --- /dev/null +++ b/CDP-LTS-80/Contacts/ContactFunctions.cs @@ -0,0 +1,81 @@ +using System; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace CDP +{ + public class ContactFunctions + { + private readonly ILogger _logger; + + public ContactFunctions(ILogger logger) + { + _logger = logger; + } + + [Function("GetContacts")] + public async Task GetContacts([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) + { + _logger.LogInformation("Get Contacts invoked"); + + // Convert the JSON payload to a string + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + GetContactsDto dto = JsonConvert.DeserializeObject(requestBody); + if (dto == null) + return new BadRequestObjectResult(new { error = true, message = "Parse error." }); + + string userId = Helpers.HashAndShortenText(dto.Email.ToLower()); + // userId = "user-" + Helpers.HashToHex(dto.Email.ToLower()); + + List fr = await ContactsDB.GetUserContacts(dto.AppKey, userId); + if (fr == null) + { + return new BadRequestObjectResult(new { error = true, message = "File not found " + dto.Email }); + } + + return new OkObjectResult(fr); + } + + [Function("AddContact")] + public async Task AddContacts([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) + { + _logger.LogInformation("Add Contacts invoked"); + + // Convert the JSON payload to a string + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + AddUserContactsDto addContactdto = JsonConvert.DeserializeObject(requestBody); + if (addContactdto == null) + return new BadRequestObjectResult(new { error = true, message = "Parse error." }); + + string userId = Helpers.HashAndShortenText(addContactdto.EmailId.ToLower()); + ContactRecord cr = new ContactRecord(addContactdto.ContactName, addContactdto.ContactEmail, addContactdto.ContactPhone, addContactdto.ContactAddress, addContactdto.ContactCompany); + + Boolean resp = await ContactsDB.AppendRecord(addContactdto.AppKey, userId, cr); + + return new OkObjectResult(resp); + } + + [Function("DeleteContact")] + public async Task DeleteContact([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) + { + _logger.LogInformation("Deleting Contact invoked"); + + // Convert the JSON payload to a string + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + DeleteUserContactsDto deleteContactdto = JsonConvert.DeserializeObject(requestBody); + if (deleteContactdto == null) + return new BadRequestObjectResult(new { error = true, message = "Parse error." }); + + string userId = Helpers.HashAndShortenText(deleteContactdto.UserEmail.ToLower()); + Boolean resp = await ContactsDB.RemoveRecord(deleteContactdto.AppKey, userId, deleteContactdto.ContactEmail.ToLower()); + await CDPDB.revokePolicies(deleteContactdto.AppKey, deleteContactdto.UserEmail.ToLower(), deleteContactdto.ContactEmail.ToLower()); + + return new OkObjectResult(resp); + } + } +} diff --git a/CDP-LTS-80/Contacts/ContactsDB.cs b/CDP-LTS-80/Contacts/ContactsDB.cs new file mode 100644 index 0000000..346f434 --- /dev/null +++ b/CDP-LTS-80/Contacts/ContactsDB.cs @@ -0,0 +1,327 @@ +using Microsoft.Azure.Cosmos; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CDP +{ + public class ContactsDB + { + private static Lazy lazyClient = new Lazy(InitializeCosmosClient); + private static CosmosClient cosmosClient => lazyClient.Value; + private static string DatabaseName = "CDP"; + private static string ContainerName = "Contacts"; + + private static CosmosClient InitializeCosmosClient() + { + // Perform any initialization here + var uri = "https://cdplite.documents.azure.com:443/"; + var authKey = "VPbg8RpzyI3XwhC2o0dIUtYFs33ghxORCqZeNAyg8vg4HWUBjM41BUxP0qLFXEvFh6ewQY1uKv52ACDbsEN1AQ=="; + + return new CosmosClient(uri, authKey); + } + + public static async Task> GetUserContacts(string AppKey, string UserId) + { + var results = new List(); + Container container = cosmosClient.GetContainer(DatabaseName, ContainerName); + + // Fetch the metadata document for the customer ID + var metadataDocumentId = GetMetaDocumentKey(AppKey, UserId); + MetadataDocumentContact metadataDocument = null; + try + { + var metadataDocumentResponse = + await container.ReadItemAsync(metadataDocumentId, new PartitionKey(metadataDocumentId)); + metadataDocument = metadataDocumentResponse.Resource; + } + catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + } + catch (Exception e) + { + // Helpers.LogIt(e.Message); + } + + if (metadataDocument == null) + return results; + + // Determine the partition keys within the date range + var partitionKeysInDocument = metadataDocument.PartitionKeys; + // Fetch the audit records for each partition key within the date range + foreach (var partitionKey in partitionKeysInDocument) + { + + ItemResponse response = await container.ReadItemAsync(partitionKey, new PartitionKey(partitionKey)); + if (response == null) + continue; + + ContactsDocument t = response.Resource; + results.AddRange(t.Records); + } + + return results; + } + + public static async Task AppendRecord(string appKey, string userId, ContactRecord rec) + { + try + { + var metadataDocument = await GetMetadataDocument(appKey, userId); + if (metadataDocument == null) + return false; + + string dayKey = metadataDocument.GetLatestKey(appKey, userId); + ContactsDocument al = await GetContactDocument(dayKey); + if (al == null) + { + al = new ContactsDocument(); + al.AppKey = appKey; + al.UserId = userId; + } + al.Records.Add(rec); + await UpdateContactsDocument(al); + return true; + } + catch (Exception e) + { + return false; + } + } + + public static async Task RemoveRecord(string appKey, string userId, string EmailId) + { + Container container = cosmosClient.GetContainer(DatabaseName, ContainerName); + + // Fetch the metadata document for the customer ID + var metadataDocumentId = GetMetaDocumentKey(appKey, userId); + MetadataDocumentContact metadataDocument = null; + try + { + var metadataDocumentResponse = await container.ReadItemAsync(metadataDocumentId, new PartitionKey(metadataDocumentId)); + metadataDocument = metadataDocumentResponse.Resource; + } + catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + } + catch (Exception e) + { + // Helpers.LogIt(e.Message); + } + + if (metadataDocument == null) + return false; + + // Determine the partition keys within the date range + var partitionKeysInDocument = metadataDocument.PartitionKeys; + // Fetch the audit records for each partition key within the date range + foreach (var partitionKey in partitionKeysInDocument) + { + + ItemResponse response = await container.ReadItemAsync(partitionKey, new PartitionKey(partitionKey)); + if (response == null) + continue; + + ContactsDocument t = response.Resource; + var originalCount = t.Records.Count; + t.Records = t.Records.Where(item => item.Email != EmailId).ToList(); + var afterFilterCount = t.Records.Count; + + if (originalCount != afterFilterCount) + { + ItemResponse updateResponse = await container.ReplaceItemAsync( + item: t, + id: t.id, + partitionKey: new PartitionKey(partitionKey)); + return true; + } + } + + return false; + } + + public static async Task UpdateContactsDocument(ContactsDocument al) + { + if (al.Records.Count == 0) + return; + + List lal = await SplitAuditlog(al); + + Container container = cosmosClient.GetContainer(DatabaseName, ContainerName); + + foreach (ContactsDocument ial in lal) + { + ItemResponse r = await container.UpsertItemAsync(ial, new PartitionKey(ial.id)); + } + + await UpdateMetadata(container, lal); + } + + static async Task UpdateMetadata(Container container, List lal) + { + bool update = false; + string pKey = GetMetaDocumentKey(lal[0].AppKey, lal[0].UserId); + MetadataDocument md = null; + try + { + ItemResponse response = + await container.ReadItemAsync(pKey, new PartitionKey(pKey)); + md = response.Resource; + } + catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + md = new MetadataDocument() + { + id = GetMetaDocumentKey(lal[0].AppKey, lal[0].UserId), + PartitionKeys = new List() + }; + } + catch (Exception e) + { + // Helpers.LogIt(e.Message); + return; + } + + if (md == null) + { + // Helpers.LogIt("Something ugly happened!"); + return; + } + + foreach (ContactsDocument log in lal) + { + if (md.PartitionKeys.Contains(log.id)) + continue; + md.PartitionKeys.Add(log.id); + update = true; + } + + if (update) + { + try + { + ItemResponse r = await container.UpsertItemAsync(md, new PartitionKey(pKey)); + } + catch (Exception e) + { + // Helpers.LogIt(e.Message); + return; + } + } + } + + static async Task> SplitAuditlog(ContactsDocument al) + { + List lal = new List(); + var sortedRecords = al.Records.OrderBy(record => record.EventTime).ToList(); + + var currentGroup = new List(); + var currentGroupSize = 0; + + int MaxDocumentSizeInBytes = 2 * 1024 * 1024; // 2MB + + int index = al.Index; // start for index passed in + foreach (var record in sortedRecords) + { + var recordSize = record.CalculateRecordSize(); + + if (currentGroupSize + recordSize > MaxDocumentSizeInBytes) + { + ContactsDocument i = new ContactsDocument(); + i.Index = index++; + i.Records = currentGroup; + i.AppKey = al.AppKey; + i.UserId = al.UserId; + lal.Add(i); + + currentGroup = new List(); + currentGroupSize = 0; + } + + currentGroup.Add(record); + currentGroupSize += recordSize; + } + + if (currentGroup.Any()) + { + ContactsDocument i = new ContactsDocument(); + i.Index = index++; + i.Records = currentGroup; + i.AppKey = al.AppKey; + i.UserId = al.UserId; + lal.Add(i); + } + + return lal; + } + + + public static async Task GetContactDocument(string key) + { + try + { + Container container = cosmosClient.GetContainer(DatabaseName, ContainerName); + ItemResponse response = await container.ReadItemAsync(key, new PartitionKey(key)); + if (response == null) + return null; + + ContactsDocument t = response.Resource; + return t; + } + catch (Exception e) + { + return null; + } + } + + static async Task GetMetadataDocument(string appKey, string userId) + { + MetadataDocumentContact md = null; + string pKey = GetMetaDocumentKey(appKey, userId); + string id = GetDocumentId(appKey, userId); + + PartitionKey partitionKey = new PartitionKeyBuilder() + .Add(pKey) + .Build(); + Container container = cosmosClient.GetContainer(DatabaseName, ContainerName); + try + { + ItemResponse response = + await container.ReadItemAsync(pKey, partitionKey); + md = response.Resource; + } + catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + md = new MetadataDocumentContact() + { + id = id, + PartitionKeys = new List() + }; + } + catch (Exception e) + { + // Helpers.LogIt(e.Message); + return null; + } + + if (md == null) + { + // Helpers.LogIt("Something ugly happened!"); + return null; + } + return md; + } + + static string GetMetaDocumentKey(string appKey, string userId) + { + return $"{appKey}-{userId}-meta"; + } + + static string GetDocumentId(string appKey, string userId) + { + return $"{appKey}-{userId}"; + } + } +} diff --git a/CDP-LTS-80/ContactsDocument.cs b/CDP-LTS-80/Contacts/ContactsDocument.cs similarity index 100% rename from CDP-LTS-80/ContactsDocument.cs rename to CDP-LTS-80/Contacts/ContactsDocument.cs diff --git a/CDP-LTS-80/EventProcessor.cs b/CDP-LTS-80/EventBased/EventProcessor.cs similarity index 96% rename from CDP-LTS-80/EventProcessor.cs rename to CDP-LTS-80/EventBased/EventProcessor.cs index f738ba2..207e8c8 100644 --- a/CDP-LTS-80/EventProcessor.cs +++ b/CDP-LTS-80/EventBased/EventProcessor.cs @@ -77,7 +77,7 @@ namespace CDP internal async Task ProcessAuditEvent(Job job) { AuditEventMetadata auditEventMetadata = JsonSerializer.Deserialize(job.JobMetadata); - await CDPLite.AddAudits(job.AppKey, auditEventMetadata.FileId, auditEventMetadata.FileName, auditEventMetadata.UserId, "", auditEventMetadata.Action, auditEventMetadata.Message); + await AuditFunctions.AddAudits(job.AppKey, auditEventMetadata.FileId, auditEventMetadata.FileName, auditEventMetadata.UserId, "", auditEventMetadata.Action, auditEventMetadata.Message); return; } @@ -202,8 +202,9 @@ namespace CDP public class MailRecord { public required string AppKey { get; set; } - public required string MailId { get; set; } public required string SenderEmail { get; set; } + public required string MailId { get; set; } // this is actually body Id + public required string BodyContent { get; set; } // this is body content public required List Attachments { get; set; } public required List ReceiverEmails { get; set; } public required List AttachmentDetails { get; set; } diff --git a/CDP-LTS-80/EventBased/MetaProcessor.cs b/CDP-LTS-80/EventBased/MetaProcessor.cs new file mode 100644 index 0000000..4d62e23 --- /dev/null +++ b/CDP-LTS-80/EventBased/MetaProcessor.cs @@ -0,0 +1,311 @@ +using Azure.Messaging.ServiceBus; +using Microsoft.Extensions.Logging; +using System; +using System.Text.Json; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Azure.Storage.Blobs; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Azure.Functions.Worker; +using System.Net; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; +using FromBodyAttribute = Microsoft.Azure.Functions.Worker.Http.FromBodyAttribute; + +namespace CDP +{ + public class MetaProcessor + { + private readonly ILogger _logger; + private static Lazy lazyBusClient = new Lazy(InitializeServiceBusClient); + private static ServiceBusClient _serviceBusClient = lazyBusClient.Value; + + public MetaProcessor(ILogger logger) + { + _logger = logger; + } + + private static ServiceBusClient InitializeServiceBusClient() + { + return new ServiceBusClient(Constants.SvcBusConnectionString); + } + + public static async Task PublishBatchJobs(List jobs) + { + //Job[] jobsCopy = jobs.ToArray(); + var sender = _serviceBusClient.CreateSender("mail-events-queue"); + ServiceBusMessageBatch batch = await sender.CreateMessageBatchAsync(); + jobs.ForEach(job => + { + var msg = new ServiceBusMessage(JsonSerializer.Serialize(job)); + bool isAdded = batch.TryAddMessage(msg); + }); + await sender.SendMessagesAsync(batch); + + } + + public static async Task PublishJob(Job job) + { + var sender = _serviceBusClient.CreateSender("mail-events-queue"); + ServiceBusMessage msg = new ServiceBusMessage(JsonSerializer.Serialize(job)); + await sender.SendMessageAsync(msg); + } + [Function("MailMetaProcessor")] + public async Task MailMetaProcessor([ActivityTrigger] MailRecord mailRecord, FunctionContext functionContext) + { + Job job = await TriggerMailProcessor(mailRecord); + return job; + } + + [Function("MailMetaOrchestrator")] + public async Task MailMetaOrchestrator([OrchestrationTrigger] TaskOrchestrationContext ctx) + { + try + { + MailRecord record = ctx.GetInput(); + string jobId = await ctx.CallActivityAsync(nameof(AddProtectionAudits), record); + Job job = await ctx.CallActivityAsync(nameof(MailMetaProcessor), record); + string revId = await ctx.CallActivityAsync(nameof(AddMailCompundRevision), record); + return job; + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + return new Job() { AppKey = "", EventType = JobType.MailMetaProcessing, Id = "failed" }; + } + } + + [Function("AddMailMetaDurable")] + public async Task AddMailMetaDurable([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, + [FromBody] MailRecord recordMail, + [DurableClient] DurableTaskClient client) + { + bool isValid = true; + //bool isValid = await CDPLite.CheckJwtRequest(req); + + if (!isValid) + { + HttpResponseData res = req.CreateResponse(HttpStatusCode.Unauthorized); + return res; + } + try + { + string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(MailMetaOrchestrator), recordMail); + _logger.LogInformation("created instance: {0}", instanceId); + return client.CreateCheckStatusResponse(req, instanceId); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + HttpResponseData response = req.CreateResponse(HttpStatusCode.InternalServerError); + return response; + } + } + + [Function("AddMailMeta")] + public async Task AddMailMeta([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, [FromBody] MailRecord recordMail) + { + bool isValid = true; + //bool isValid = await CDPLite.CheckJwtRequest(req); + if (!isValid) + { + return new UnauthorizedObjectResult("Jwt has expired please validate"); + } + try + { + MailRecord mailRecord = recordMail; + //_logger.LogInformation(JsonSerializer.Serialize(recordMail)); + string res = await TriggerProtectionAuditsJob(mailRecord); + res = await AddMailCompoundRevisionInternal(mailRecord); + _logger.LogInformation($"protection audits? {res}"); + Job job = await TriggerMailProcessor(mailRecord); + return new OkObjectResult(job); + + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return new StatusCodeResult(500); + } + + } + + [Function("AddMailForwardingRequest")] + public async Task AddMailForwardingRequest([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req) + { + ForwardMailDto forwardMail; + try + { + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + _logger.LogInformation("requestbody: {0}", requestBody); + forwardMail = JsonSerializer.Deserialize(requestBody); + } + catch (Exception ex) + { + _logger.LogInformation(ex.ToString()); + return new StatusCodeResult(500); + } + string appKey = forwardMail.AppKey; + var userId = Helpers.HashAndShortenText(forwardMail.SenderEmail.ToLower()); + var fr = await CDPDB.GetFile(appKey, forwardMail.MailId, userId); + if ((!fr.Policy.CheckAccess("Manage")) && (!fr.Policy.CheckAccess("Owner"))) + { + return new UnauthorizedObjectResult("User doesn't have rights to forward this email"); + } + RevisionEntry entry = await FileRevision.GetFileRevisionInternal(forwardMail.AppKey, forwardMail.MailId); + if (entry.CompoundDocument == null) + { + return new BadRequestObjectResult("IncorrectMailId"); + } + MailMetaRecord mailRecord = JsonSerializer.Deserialize(entry.CompoundDocument.Contents); + + mailRecord.AttachmentDetails.ForEach(async (att) => + { + forwardMail.ReceiverEmails.ForEach(async (receiver) => + { + await AddMailForwardPolicy(appKey, att.FileId, att.FileName, forwardMail.SenderEmail, receiver, "Read"); + }); + }); + + forwardMail.ReceiverEmails.ForEach(async (receiver) => + { + await AddMailForwardPolicy(appKey, mailRecord.BodyId, string.Concat(mailRecord.BodyId, "-", "MailBody"), forwardMail.SenderEmail, receiver, "Read"); + }); + + return new OkObjectResult("Done"); + } + + internal async Task AddMailForwardPolicy(string appKey, string fileId, string fileName, string sender, string receiver, string policy = "Read") + { + AddFileUserDto addFileUserDto = new AddFileUserDto + { + AppKey = appKey, + FileId = fileId, + FileName = fileName, + Email = sender, + EmailToAdd = receiver, + Policy = policy + }; + await CDPLite.AddFileUserInternal(addFileUserDto); + } + + [Function("AddMailCompundRevision")] + public async Task AddMailCompundRevision([ActivityTrigger] MailRecord record, FunctionContext context) + { + + return await AddMailCompoundRevisionInternal(record); + } + + internal async Task AddMailCompoundRevisionInternal(MailRecord record) + { + RevisionDocument doc = new RevisionDocument + { + RevisionId = Guid.NewGuid().ToString(), + RevisionDate = DateTime.UtcNow, + EditedBy = record.SenderEmail, + Comments = "MailBody" + }; + MailMetaRecord mailMeta = new MailMetaRecord + { + AppKey = record.AppKey, + SenderEmail = record.SenderEmail, + BodyContent = record.BodyContent, + BodyId = record.MailId, + AttachmentDetails = record.AttachmentDetails + }; + CompoundDocument compound = new CompoundDocument + { + Id = Guid.NewGuid().ToString(), + AppKey = record.AppKey, + Contents = JsonSerializer.Serialize(mailMeta), + DocumentType = "MailRecord" + }; + + string revisionId = await FileRevision.AddMailBodyRevision(doc, compound, record.AppKey, record.MailId); + return revisionId; + } + + [Function("AddProtectionAudits")] + public async Task AddProtectionAudits([ActivityTrigger] MailRecord record, FunctionContext functionContext) + { + var res = await TriggerProtectionAuditsJob(record); + return res; + } + + internal async Task TriggerProtectionAuditsJob(MailRecord record) + { + try + { + string userId = Helpers.HashAndShortenText(record.SenderEmail); + string message = string.Format($"{record.SenderEmail} protected an email body with id: {record.MailId}."); + List auditEvents = new List(); + AuditEventMetadata auditEvent = new AuditEventMetadata { Action = "Addded", FileId = record.MailId, FileName = "Outlook-Mail-Body", Message = message, UserId = userId }; + auditEvents.Add(auditEvent); + record.AttachmentDetails.ForEach((att) => + { + message = string.Format($"{record.SenderEmail} protected the email attachment {att.FileName} with id: {att.FileId}."); + AuditEventMetadata attachmentEvent = new AuditEventMetadata { Action = "Added", FileId = att.FileId, FileName = att.FileName, Message = message, UserId = userId }; + auditEvents.Add(attachmentEvent); + }); + Job job = await CDPLite.AddAuditsBatchedEvent(auditEvents, record.AppKey); + await PublishJob(job); + _logger.LogInformation(JsonSerializer.Serialize(job)); + return "Done"; + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + throw ex; + } + } + + internal async Task TriggerMailProcessor(MailRecord mailRecord) + { + string Connection = Constants.StorageConnString; + var blobSvc = new BlobServiceClient(Connection); + var blobClient = blobSvc.GetBlobContainerClient(mailRecord.AppKey.ToLower()); + + if (!blobClient.Exists()) + { + await blobClient.CreateAsync(); + } + byte[] fileBytes = System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(mailRecord)); + string jobId = Guid.NewGuid().ToString(); + var blob = blobClient.GetBlobClient(jobId); + Stream fs = new MemoryStream(fileBytes); + try + { + await blob.UploadAsync(fs); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + throw ex; + } + + Job job = new Job { AppKey = mailRecord.AppKey, EventType = JobType.MailMetaProcessing, Id = jobId }; + await PublishJob(job); + return job; + } + } + + public class MailMetaRecord + { + public required string AppKey { get; set; } + public required string SenderEmail { get; set; } + public required string BodyId { get; set; } + public required string BodyContent { get; set; } //encrypted body + public required List AttachmentDetails { get; set; } + } + + public class ForwardMailDto + { + public string SenderEmail { get; set; } + public List ReceiverEmails { get; set; } + public string MailId { get; set; } + public string AppKey { get; set; } + } +} diff --git a/CDP-LTS-80/Groups/GroupsDB.cs b/CDP-LTS-80/Groups/GroupsDB.cs new file mode 100644 index 0000000..a155f2c --- /dev/null +++ b/CDP-LTS-80/Groups/GroupsDB.cs @@ -0,0 +1,388 @@ +using Microsoft.Azure.Cosmos; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CDP +{ + public class GroupsDB + { + private static Lazy lazyClient = new Lazy(InitializeCosmosClient); + private static CosmosClient cosmosClient => lazyClient.Value; + private static string DatabaseName = "CDP"; + private static string ContainerName = "Groups"; + + private static CosmosClient InitializeCosmosClient() + { + // Perform any initialization here + var uri = "https://cdplite.documents.azure.com:443/"; + var authKey = "VPbg8RpzyI3XwhC2o0dIUtYFs33ghxORCqZeNAyg8vg4HWUBjM41BUxP0qLFXEvFh6ewQY1uKv52ACDbsEN1AQ=="; + + return new CosmosClient(uri, authKey); + } + + public static async Task> GetUserGroups(string AppKey, string UserId) + { + var results = new List(); + Container container = cosmosClient.GetContainer(DatabaseName, ContainerName); + + // Fetch the metadata document for the customer ID + var metadataDocumentId = GetMetaDocumentKey(AppKey, UserId); + MetadataDocumentGroups metadataDocument = null; + try + { + var metadataDocumentResponse = + await container.ReadItemAsync(metadataDocumentId, new PartitionKey(metadataDocumentId)); + metadataDocument = metadataDocumentResponse.Resource; + } + catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + } + catch (Exception e) + { + // Helpers.LogIt(e.Message); + } + + if (metadataDocument == null) + return results; + + // Determine the partition keys within the date range + var partitionKeysInDocument = metadataDocument.PartitionKeys; + // Fetch the audit records for each partition key within the date range + foreach (var partitionKey in partitionKeysInDocument) + { + + ItemResponse response = await container.ReadItemAsync(partitionKey, new PartitionKey(partitionKey)); + if (response == null) + continue; + + GroupsDocument t = response.Resource; + results.AddRange(t.Records); + } + + return results; + } + + public static async Task RemoveGroup(string appKey, string userId, string groupId) + { + Container container = cosmosClient.GetContainer(DatabaseName, ContainerName); + + // Fetch the metadata document for the customer ID + var metadataDocumentId = GetMetaDocumentKey(appKey, userId); + MetadataDocumentContact metadataDocument = null; + try + { + var metadataDocumentResponse = await container.ReadItemAsync(metadataDocumentId, new PartitionKey(metadataDocumentId)); + metadataDocument = metadataDocumentResponse.Resource; + } + catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + } + catch (Exception e) + { + // Helpers.LogIt(e.Message); + } + + if (metadataDocument == null) + return false; + + // Determine the partition keys within the date range + var partitionKeysInDocument = metadataDocument.PartitionKeys; + // Fetch the audit records for each partition key within the date range + foreach (var partitionKey in partitionKeysInDocument) + { + + ItemResponse response = await container.ReadItemAsync(partitionKey, new PartitionKey(partitionKey)); + if (response == null) + continue; + + GroupsDocument t = response.Resource; + var originalCount = t.Records.Count; + t.Records = t.Records.Where(item => item.id != groupId).ToList(); + var afterFilterCount = t.Records.Count; + + if (originalCount != afterFilterCount) + { + ItemResponse updateResponse = await container.ReplaceItemAsync( + item: t, + id: t.id, + partitionKey: new PartitionKey(partitionKey)); + return true; + } + } + + return false; + } + + public static async Task AppendGroup(string appKey, string userId, String grpName, string grpDescription, List rec) + { + try + { + var metadataDocument = await GetMetadataDocument(appKey, userId); + if (metadataDocument == null) + return false; + + string dayKey = metadataDocument.GetLatestKey(appKey, userId); + GroupsDocument al = await GetGroupDocument(dayKey); + if (al == null) + { + al = new GroupsDocument(); + al.AppKey = appKey; + al.UserId = userId; + } + GroupsRecord gr = new GroupsRecord(grpName, grpDescription, rec); + al.Records.Add(gr); + await UpdateGroupsDocument(al); + return true; + } + catch (Exception e) + { + return false; + } + } + + public static async Task UpdateGroup(string appKey, string userId, string grpId, string grpName, string grpDescription, List contacts) + { + Container container = cosmosClient.GetContainer(DatabaseName, ContainerName); + + // Fetch the metadata document for the customer ID + var metadataDocumentId = GetMetaDocumentKey(appKey, userId); + MetadataDocumentContact metadataDocument = null; + try + { + var metadataDocumentResponse = await container.ReadItemAsync(metadataDocumentId, new PartitionKey(metadataDocumentId)); + metadataDocument = metadataDocumentResponse.Resource; + } + catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + } + catch (Exception e) + { + // Helpers.LogIt(e.Message); + } + + if (metadataDocument == null) + return false; + + // Determine the partition keys within the date range + var partitionKeysInDocument = metadataDocument.PartitionKeys; + // Fetch the audit records for each partition key within the date range + foreach (var partitionKey in partitionKeysInDocument) + { + + ItemResponse response = await container.ReadItemAsync(partitionKey, new PartitionKey(partitionKey)); + if (response == null) + continue; + + GroupsDocument t = response.Resource; + bool groupFound = false; + foreach (GroupsRecord gr in t.Records) + { + if (gr.id == grpId) + { + gr.Name = grpName != "" ? grpName : gr.Name; + gr.Description = grpDescription != "" ? grpDescription : gr.Description; + gr.Contacts = contacts; + groupFound = true; + break; + } + } + + if (groupFound) + { + ItemResponse updateResponse = await container.ReplaceItemAsync( + item: t, + id: t.id, + partitionKey: new PartitionKey(partitionKey)); + return true; + } + } + + return false; + } + + public static async Task UpdateGroupsDocument(GroupsDocument al) + { + if (al.Records.Count == 0) + return; + + List lal = await SplitUserGroups(al); + + Container container = cosmosClient.GetContainer(DatabaseName, ContainerName); + + foreach (GroupsDocument ial in lal) + { + ItemResponse r = await container.UpsertItemAsync(ial, new PartitionKey(ial.id)); + } + + await UpdateMetadata(container, lal); + } + + static async Task UpdateMetadata(Container container, List lal) + { + bool update = false; + string pKey = GetMetaDocumentKey(lal[0].AppKey, lal[0].UserId); + MetadataDocument md = null; + try + { + ItemResponse response = + await container.ReadItemAsync(pKey, new PartitionKey(pKey)); + md = response.Resource; + } + catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + md = new MetadataDocument() + { + id = GetMetaDocumentKey(lal[0].AppKey, lal[0].UserId), + PartitionKeys = new List() + }; + } + catch (Exception e) + { + // Helpers.LogIt(e.Message); + return; + } + + if (md == null) + { + // Helpers.LogIt("Something ugly happened!"); + return; + } + + foreach (GroupsDocument log in lal) + { + if (md.PartitionKeys.Contains(log.id)) + continue; + md.PartitionKeys.Add(log.id); + update = true; + } + + if (update) + { + try + { + ItemResponse r = await container.UpsertItemAsync(md, new PartitionKey(pKey)); + } + catch (Exception e) + { + // Helpers.LogIt(e.Message); + return; + } + } + } + + static async Task> SplitUserGroups(GroupsDocument al) + { + List lal = new List(); + var sortedRecords = al.Records.OrderBy(record => record.EventTime).ToList(); + + var currentGroup = new List(); + var currentGroupSize = 0; + + int MaxDocumentSizeInBytes = 2 * 1024 * 1024; // 2MB + + int index = al.Index; // start for index passed in + foreach (var record in sortedRecords) + { + var recordSize = record.CalculateRecordSize(); + + if (currentGroupSize + recordSize > MaxDocumentSizeInBytes) + { + GroupsDocument i = new GroupsDocument(); + i.Index = index++; + i.Records = currentGroup; + i.AppKey = al.AppKey; + i.UserId = al.UserId; + lal.Add(i); + + currentGroup = new List(); + currentGroupSize = 0; + } + + currentGroup.Add(record); + currentGroupSize += recordSize; + } + + if (currentGroup.Any()) + { + GroupsDocument i = new GroupsDocument(); + i.Index = index++; + i.Records = currentGroup; + i.AppKey = al.AppKey; + i.UserId = al.UserId; + lal.Add(i); + } + + return lal; + } + + + public static async Task GetGroupDocument(string key) + { + try + { + Container container = cosmosClient.GetContainer(DatabaseName, ContainerName); + ItemResponse response = await container.ReadItemAsync(key, new PartitionKey(key)); + if (response == null) + return null; + + GroupsDocument t = response.Resource; + return t; + } + catch (Exception e) + { + return null; + } + } + + static async Task GetMetadataDocument(string appKey, string userId) + { + MetadataDocumentGroups md = null; + string pKey = GetMetaDocumentKey(appKey, userId); + string id = GetDocumentId(appKey, userId); + + PartitionKey partitionKey = new PartitionKeyBuilder() + .Add(pKey) + .Build(); + Container container = cosmosClient.GetContainer(DatabaseName, ContainerName); + try + { + ItemResponse response = + await container.ReadItemAsync(pKey, partitionKey); + md = response.Resource; + } + catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + md = new MetadataDocumentGroups() + { + id = id, + PartitionKeys = new List() + }; + } + catch (Exception e) + { + // Helpers.LogIt(e.Message); + return null; + } + + if (md == null) + { + // Helpers.LogIt("Something ugly happened!"); + return null; + } + return md; + } + + static string GetMetaDocumentKey(string appKey, string userId) + { + return $"{appKey}-{userId}-meta"; + } + + static string GetDocumentId(string appKey, string userId) + { + return $"{appKey}-{userId}"; + } + } +} diff --git a/CDP-LTS-80/GroupsDocument.cs b/CDP-LTS-80/Groups/GroupsDocument.cs similarity index 100% rename from CDP-LTS-80/GroupsDocument.cs rename to CDP-LTS-80/Groups/GroupsDocument.cs diff --git a/CDP-LTS-80/Groups/GroupsFunctions.cs b/CDP-LTS-80/Groups/GroupsFunctions.cs new file mode 100644 index 0000000..0a1b5ef --- /dev/null +++ b/CDP-LTS-80/Groups/GroupsFunctions.cs @@ -0,0 +1,107 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace CDP +{ + public class GroupsFunctions + { + private readonly ILogger _logger; + + public GroupsFunctions(ILogger logger) + { + _logger = logger; + } + + [Function("AddGroup")] + public async Task AddGroup([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) + { + _logger.LogInformation("Adding a new group invoked"); + + // Convert the JSON payload to a string + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + AddUserGroupDto addUserGroupDto = JsonConvert.DeserializeObject(requestBody); + if (addUserGroupDto == null) + return new BadRequestObjectResult(new { error = true, message = "Parse error." }); + + string userId = Helpers.HashAndShortenText(addUserGroupDto.EmailId.ToLower()); + + List contacts = new List(); + foreach (Contact contact in addUserGroupDto.Contacts) + { + ContactRecord cr = new ContactRecord(contact.Name, contact.Email, contact.Phone, contact.Address, contact.Company); + contacts.Add(cr); + } + + Boolean resp = await GroupsDB.AppendGroup(addUserGroupDto.AppKey, userId, addUserGroupDto.GroupName, addUserGroupDto.GroupDescription, contacts); + + return new OkObjectResult(resp); + } + + [Function("GetGroups")] + public async Task GetGroups([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) + { + _logger.LogInformation("Getting groups invoked"); + + // Convert the JSON payload to a string + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + GetUserGroupDto getUserGroupDto = JsonConvert.DeserializeObject(requestBody); + if (getUserGroupDto == null) + return new BadRequestObjectResult(new { error = true, message = "Parse error." }); + + string userId = Helpers.HashAndShortenText(getUserGroupDto.EmailId.ToLower()); + + List fr = await GroupsDB.GetUserGroups(getUserGroupDto.AppKey, userId); + if (fr == null) + { + return new BadRequestObjectResult(new { error = true, message = "groups not found " + getUserGroupDto.EmailId }); + } + + return new OkObjectResult(fr); + } + + [Function("DeleteGroup")] + public async Task DeleteGroup([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) + { + _logger.LogInformation("Deleting Group invoked"); + + // Convert the JSON payload to a string + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + DeleteGroupDto deleteGroupdto = JsonConvert.DeserializeObject(requestBody); + if (deleteGroupdto == null) + return new BadRequestObjectResult(new { error = true, message = "Parse error." }); + + string userId = Helpers.HashAndShortenText(deleteGroupdto.UserEmail.ToLower()); + Boolean resp = await GroupsDB.RemoveGroup(deleteGroupdto.AppKey, userId, deleteGroupdto.GroupId); + return new OkObjectResult(resp); + } + + [Function("UpdateGroup")] + public async Task UpdateGroup([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) + { + _logger.LogInformation("Updating Group invoked"); + + // Convert the JSON payload to a string + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + UpdateGroupDto updateUserGroupDto = JsonConvert.DeserializeObject(requestBody); + if (updateUserGroupDto == null) + return new BadRequestObjectResult(new { error = true, message = "Parse error." }); + + string userId = Helpers.HashAndShortenText(updateUserGroupDto.UserEmail.ToLower()); + + List contacts = new List(); + foreach (Contact contact in updateUserGroupDto.Contacts) + { + ContactRecord cr = new ContactRecord(contact.Name, contact.Email, contact.Phone, contact.Address, contact.Company); + contacts.Add(cr); + } + + Boolean resp = await GroupsDB.UpdateGroup(updateUserGroupDto.AppKey, userId, updateUserGroupDto.GroupId, updateUserGroupDto.GroupName, updateUserGroupDto.GroupDescription, contacts); + + return new OkObjectResult(resp); + } + } +} diff --git a/CDP-LTS-80/Helpers.cs b/CDP-LTS-80/Helpers.cs index 44dd15e..661d64e 100644 --- a/CDP-LTS-80/Helpers.cs +++ b/CDP-LTS-80/Helpers.cs @@ -9,9 +9,48 @@ using System.Text; using System.Threading.Tasks; using Azure.Identity; using Azure.Security.KeyVault.Secrets; +using System.Diagnostics; namespace CDP { + public class MethodTimer : IDisposable + { + private readonly Stopwatch stopwatch = new Stopwatch(); + private readonly string methodName; + + public MethodTimer(string methodName) + { + this.methodName = methodName; + Start(); + } + + public void Dispose() + { + Stop(); + } + + public void Start() + { + stopwatch.Start(); + } + + public void Stop() + { + stopwatch.Stop(); + LogElapsedTime(); + } + + private void LogElapsedTime() + { + Dictionary log = new Dictionary + { + {"MethodName", methodName}, + {"ExecutionTime", stopwatch.ElapsedMilliseconds}, + }; + Console.WriteLine($"\n{JsonConvert.SerializeObject(log)}\n"); + // You can customize the logging mechanism (e.g., use a logging library, write to a file, etc.) based on your requirements. + } + } public class Helpers { public static byte[] GenerateAES256Key() diff --git a/CDP-LTS-80/host.json b/CDP-LTS-80/host.json index ee5cf5f..ecdcba7 100644 --- a/CDP-LTS-80/host.json +++ b/CDP-LTS-80/host.json @@ -1,12 +1,15 @@ { - "version": "2.0", - "logging": { - "applicationInsights": { - "samplingSettings": { - "isEnabled": true, - "excludedTypes": "Request" - }, - "enableLiveMetricsFilters": true - } + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + }, + "enableLiveMetricsFilters": true } + }, + "tracing": { + "consoleLevel": "verbose" + } } \ No newline at end of file