using EnVisage.Code.Exceptions; using Newtonsoft.Json; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using System.Web.Mvc; using System.Xml; using System.Xml.Serialization; using Audit.Core; using Audit.Mvc; using EnVisage.Code.Audit.Attributes; using EnVisage.Code.Audit.Configuration; using EnVisage.Code.Audit.DataProviders; using Prevu.Core.Audit.Model; using NLog; using Prevu.Core.Audit.Model.ResponseModels; namespace EnVisage.Code { public class AuditProxy { #region Private private static int _unauthorizedRequestsCount = 0; private static readonly int _maxUnauthorizedAttempts = 2; private static string _apiUrl = null; private static string _tenantId = null; private static string _password = null; private static string _domainId = null; private static string _accessToken = null; private static readonly ConcurrentDictionary> _historyCache = new ConcurrentDictionary>(); private static readonly ConcurrentDictionary> _eventCache = new ConcurrentDictionary>(); private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); #endregion #region Models public class AuditSaveModel { public string EntityId { get; set; } public string UserId { get; set; } public string Data { get; set; } public int Version { get; set; } public string TenantId { get; set; } public string DomainId { get; set; } public string EventType { get; set; } public Int64 Duration { get; set; } public int? ResponseStatusCode { get; set; } } public class HistorySaveModel { public string TransactionId { get; set; } public string ModifiedBy { get; set; } public string DomainId { get; set; } public string TenantId { get; set; } public List Entities { get; set; } } public class HistorySaveModelEntity { public string EntityId { get; set; } public string Details { get; set; } } public class HistorySaveDetailType { public string DetailType { get; set; } public List Details { get; set; } } public class HistorySaveDetail { [XmlIgnore] public string GroupKey { get; set; } public string DetailId { get; set; } [XmlIgnore] public string DetailType { get; set; } public string ModificationType { get; set; } public List Properties { get; set; } = new List(); } public class HistorySaveItemPropertyModel { public string Name { get; set; } public string OldValue { get; set; } public string NewValue { get; set; } } #endregion public static void Initialize(string apiUrl, string tenantId, string password, string domainId) { #region Arguments Validation if (string.IsNullOrWhiteSpace(apiUrl)) throw new ArgumentNullException(nameof(apiUrl)); if (string.IsNullOrWhiteSpace(tenantId)) throw new ArgumentNullException(nameof(tenantId)); if (string.IsNullOrWhiteSpace(password)) throw new ArgumentNullException(nameof(password)); if (string.IsNullOrWhiteSpace(domainId)) throw new ArgumentNullException(nameof(domainId)); #endregion _apiUrl = apiUrl; _tenantId = tenantId; _password = password; _domainId = domainId; } #region History public static void LogHistory(string transactionId, string groupKey, string entityId, string entityType, string modificationType, IEnumerable properties) { #region Arguments Validation if (string.IsNullOrWhiteSpace(transactionId)) throw new ArgumentNullException(nameof(transactionId)); if (string.IsNullOrWhiteSpace(groupKey) && string.IsNullOrWhiteSpace(entityId)) throw new ArgumentException("Either groupKey or entityId must exist"); if (string.IsNullOrWhiteSpace(entityType)) throw new ArgumentNullException(nameof(entityType)); if (string.IsNullOrWhiteSpace(modificationType)) throw new ArgumentNullException(nameof(modificationType)); #endregion var saveModel = new HistorySaveDetail { GroupKey = groupKey, DetailId = entityId, DetailType = entityType, ModificationType = modificationType, }; if (properties != null) saveModel.Properties.AddRange(properties); var saveModelList = new List { saveModel }; LogHistory(transactionId, saveModelList); } public static void LogHistory(string transactionId, IEnumerable entities) { #region Arguments Validation if (string.IsNullOrWhiteSpace(transactionId)) throw new ArgumentNullException(nameof(transactionId)); if (entities == null || !entities.Any()) throw new ArgumentNullException(nameof(entities)); #endregion if (!_historyCache.ContainsKey(transactionId)) _historyCache[transactionId] = new List(); _historyCache[transactionId].AddRange(entities); } public static async Task CommitHistoryChanges(string transactionId, string userId) { #region Arguments Validation if (string.IsNullOrWhiteSpace(transactionId)) throw new ArgumentNullException(nameof(transactionId)); if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId)); #endregion if (!_historyCache.ContainsKey(transactionId)) return; var entities = _historyCache[transactionId]; var details = entities.GroupBy(x => x.GroupKey ?? x.DetailId) .ToDictionary(x => x.Key, g => g.GroupBy(s => s.DetailType) .ToDictionary(s => s.Key, s => s.ToList())); var saveModel = new HistorySaveModel { DomainId = _domainId, TenantId = _tenantId, TransactionId = transactionId, ModifiedBy = userId, Entities = details.Select(x => new HistorySaveModelEntity { EntityId = x.Key, Details = x.Value.Select(s => new HistorySaveDetailType { DetailType = s.Key, Details = s.Value }).ToList().Serialize() }).ToList() }; var result = await AuthorizeAndMakeRequest(async (accessToken) => { using (var client = CreateAuditClient(accessToken)) { return await client.PostAsJsonAsync("History/Log", saveModel); } }); if (result == null) throw new Exception("LogHistory result is undefined"); if (!result.IsSuccessStatusCode) { var message = $@" Message: {result.ReasonPhrase} Model: {saveModel.Serialize()}"; _logger.Log(LogLevel.Info, message); } ClearHistoryChanges(transactionId); } public static void ClearHistoryChanges(string transactionId) { #region Arguments Validation if (string.IsNullOrWhiteSpace(transactionId)) throw new ArgumentNullException(nameof(transactionId)); #endregion var value = new List(); if (_historyCache.ContainsKey(transactionId)) _historyCache.TryRemove(transactionId, out value); } #endregion #region Audit public static async Task LogAudit(AuditEvent entity, string userId) { #region Arguments Validation if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId)); if (entity == null) throw new ArgumentNullException(nameof(entity)); #endregion var data = entity.ToJson(); var dataXml = JsonConvert.DeserializeXmlNode(data, "root"); var saveModel = new AuditSaveModel { DomainId = _domainId, TenantId = _tenantId, UserId = userId, Data = dataXml.InnerXml, Duration = entity.Duration, EventType = entity.EventType, ResponseStatusCode = entity.GetMvcAuditAction().ResponseStatusCode }; var result = await AuthorizeAndMakeRequest(async (accessToken) => { using (var client = CreateAuditClient(accessToken)) { return await client.PostAsJsonAsync("Audit/Log", saveModel); } }); if (result == null) throw new Exception("LogHistory result is undefined"); if (!result.IsSuccessStatusCode) { var message = $@" Message: {result.ReasonPhrase} Model: {saveModel.Serialize()}"; _logger.Log(LogLevel.Info, message); } } public static void InitializeAudit() { var configuration = AuditConfigurationSection.Current; if (configuration.Enabled) { GlobalFilters.Filters.Add(new PrevuAuditAttribute()); Configuration.Setup().UseCustomProvider(new AuditCustomDataProvider()); } } #endregion #region Events public static void LogEvent(string transactionId, EventRequestBaseModel eventModel) { #region Arguments Validation if (string.IsNullOrWhiteSpace(transactionId)) throw new ArgumentNullException(nameof(transactionId)); if (eventModel == null) return; #endregion if (!_eventCache.ContainsKey(transactionId)) _eventCache[transactionId] = new List(); _eventCache[transactionId].Add(eventModel); } public static void LogEvent(string transactionId, IEnumerable eventModels) { #region Arguments Validation if (string.IsNullOrWhiteSpace(transactionId)) throw new ArgumentNullException(nameof(transactionId)); if (eventModels == null || !eventModels.Any()) return; #endregion if (!_eventCache.ContainsKey(transactionId)) _eventCache[transactionId] = new List(); _eventCache[transactionId].AddRange(eventModels); } public static async Task CommitEventChanges(string transactionId) { #region Arguments Validation if (string.IsNullOrWhiteSpace(transactionId)) throw new ArgumentNullException(nameof(transactionId)); #endregion if (!_eventCache.ContainsKey(transactionId)) throw new InvalidTransactionException(transactionId); var entities = _eventCache[transactionId]; var saveModel = new List(); foreach (var entity in entities) { saveModel.Add(new EventRequestModel { TransactionId = transactionId, EntityId = entity.EntityId, DomainId = new Guid(_domainId), TenantId = new Guid(_tenantId), ClassificationKey = entity.ClassificationKey, Comment = entity.Comment, NewValue = entity.NewValue, OldValue = entity.OldValue, UserId = entity.UserId, ProjectId = entity.ProjectId, ScenarioId = entity.ScenarioId }); } var result = await AuthorizeAndMakeRequest(async (accessToken) => { using (var client = CreateAuditClient(accessToken)) { return await client.PostAsJsonAsync("event/logmany", saveModel); } }); if (result == null) throw new Exception("LogEvent result is undefined"); if (result.IsSuccessStatusCode) ClearEventChanges(transactionId); } public static void ClearEventChanges(string transactionId) { #region Arguments Validation if (string.IsNullOrWhiteSpace(transactionId)) throw new ArgumentNullException(nameof(transactionId)); #endregion var value = new List(); if (_eventCache.ContainsKey(transactionId)) _eventCache.TryRemove(transactionId, out value); } public static async Task GetEvents(Guid entityId, int take = 25, int skip = 0, string orderBy = "EntityId", bool isOrderAsc = true) { var result = await AuthorizeAndMakeRequest(async (accessToken) => { using (var client = CreateAuditClient(accessToken)) { return await client.GetAsync($"event/get/?entityId={entityId}&take={take}&skip={skip}&orderBy={orderBy}&isOrderAsc={isOrderAsc}"); } }); return await ReturnEvents(result); } public static async Task GetEvents(IEnumerable entityId, int take = 25, int skip = 0, string orderBy = "EntityId", bool isOrderAsc = true) { var result = await AuthorizeAndMakeRequest(async (accessToken) => { using (var client = CreateAuditClient(accessToken)) { var entityIdStr = string.Empty; foreach (var entityIdGuid in entityId) { entityIdStr = entityIdStr + "entityId=" + entityIdGuid + "&"; } return await client.GetAsync($"event/GetByMany/?{entityIdStr}take={take}&skip={skip}&orderBy={orderBy}&isOrderAsc={isOrderAsc}"); } }); return await ReturnEvents(result); } #region Private Event Methods private static async Task ReturnEvents(HttpResponseMessage result) { if (result == null) throw new Exception("GetEvents result is undefined"); if (result.IsSuccessStatusCode) { var responseBody = await result.Content.ReadAsStringAsync(); var response = JsonConvert.DeserializeObject>(responseBody); if (response.Success) { return response.Result; } } return new EventGetResponseModel(); } #endregion #endregion #region Private Methods private static Task GetAuthToken(string tenantId, string password) { // TODO: Audit - temp solution until we decided to create shared OAuth service return Task.Run(() => Properties.Settings.Default.TenantAccessToken); // TODO: Audit - uncomment this block when we have OAuth service //var accessTokenKey = "access_token"; //var pairs = new Dictionary //{ // { "grant_type", "password" }, // { "username", tenantId }, // { "password", password }, //}; //var content = new FormUrlEncodedContent(pairs); //using (var client = CreateAuditClient()) //{ // var response = await client.PostAsync("/Token", content); // var result = await response.Content.ReadAsStringAsync(); // var tokenDictionary = JsonConvert.DeserializeObject>(result); // if (tokenDictionary == null || !tokenDictionary.ContainsKey(accessTokenKey) || string.IsNullOrWhiteSpace(tokenDictionary[accessTokenKey])) // throw new AccessTokenNullException($"Cannot get {accessTokenKey} for tenantId = {tenantId}"); // return tokenDictionary[accessTokenKey]; //} } private static async Task AuthorizeAndMakeRequest(Func> request) { if (request == null) throw new ArgumentNullException(nameof(request)); if (string.IsNullOrWhiteSpace(_accessToken)) _accessToken = await GetAuthToken(_tenantId, _password); var result = await request(_accessToken); if (result == null) throw new Exception("Delegate request got null"); if (result.StatusCode == HttpStatusCode.Unauthorized) { _unauthorizedRequestsCount++; if (_unauthorizedRequestsCount < _maxUnauthorizedAttempts) { _accessToken = await GetAuthToken(_tenantId, _password); return await AuthorizeAndMakeRequest(request); } } else { _unauthorizedRequestsCount = 0; } return result; } private static HttpClient CreateAuditClient(string accessToken = null) { var client = new HttpClient(); client.BaseAddress = new Uri(_apiUrl); client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); if (!string.IsNullOrWhiteSpace(accessToken)) client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); return client; } #endregion } }