using CloudBuilder.Core.DatabaseAccessor; using CloudBuilder.Core.DatabaseAccessor.Entity; using CloudBuilder.Core.DependencyInjection.Request; using CloudBuilder.Core.Policy; using CloudBuilder.Core.Service; using CloudBuilder.Gui; using CloudBuilder.Security.Entity; using CloudBuilder.Security.Policy; using CloudBuilder.Security.SearchCriteria; using DocumentFormat.OpenXml.Spreadsheet; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; namespace CloudBuilder.Security.Service { public class SecurityLockService : ISecurityLockService { //if key is non string type, it is expected to convert to string before using this process //usage: //1. UI call Lock to hold entity, exception would be thrown if entity held by others //2. upon finish edition, UI call ConsumeLock to free the reserved lock, // exception would be thrown if the lock is overrided either by super user or after expiery public const double ACTIVE_WINDOW_MINUTES = -30D;//allow lock 30 minutes private SecurityMessagePolicy securityMessagePolicy; private readonly IApplicationService service; private readonly ILocaleMessageManager localeMessageManager; private readonly IContext context; private IRepository repository; public SecurityLockService(IApplicationService service, ILocaleMessageManager localeMessageManager, IContext context) { this.service = service; this.localeMessageManager = localeMessageManager; this.context = context; repository = service.GetRepository>(); securityMessagePolicy = service.ServiceProvider.GetRequiredKeyedService(typeof(SecurityMessagePolicy).Name); } [PermissionOperate(name: SecurityPermissionPolicy.SecurityLock, operate: Operate.Find)] public SecurityLockEntity FindByKey(SecurityLockEntityKey key) { return repository.DetachedEntities.Where(x => 1 == 1 && x.EntityName == key.EntityName && x.PkeyId == key.PkeyId && x.LockedByActor == key.LockedByActor ).FirstOrDefault()!; } //[PermissionOperate(name: SecurityPermissionPolicy.SecurityLock, operate: Operate.Find)] public dynamic FindBySearchCriteria(SecurityLockSearchCriteria criteria) { throw new NotImplementedException(); } //[PermissionOperate(name: SecurityPermissionPolicy.SecurityLock, operate: Operate.Print)] public dynamic PrintBySearchCriteria(SecurityLockSearchCriteria criteria) { throw new NotImplementedException(); } [PermissionOperate(name: SecurityPermissionPolicy.SecurityLock, operate: Operate.Insert)] [UnitOfWork] public SecurityLockEntity Insert(SecurityLockEntity data) { repository.InsertNow(data); return data; } [PermissionOperate(name: SecurityPermissionPolicy.SecurityLock, operate: Operate.Update)] [UnitOfWork] public SecurityLockEntity Update(SecurityLockEntity data) { throw new NotImplementedException(); } [PermissionOperate(name: SecurityPermissionPolicy.SecurityLock, operate: Operate.Delete)] public void Delete(SecurityLockEntityKey key) { SecurityLockEntity ent = repository.DetachedEntities.Where(x => 1 == 1 && x.EntityName == key.EntityName && x.PkeyId == key.PkeyId && x.LockedByActor == key.LockedByActor ).FirstOrDefault()!; if (ent != null) { repository.DeleteNow(ent); } } [AllowAnonymous] public bool IsLocked(string entityName, string key) { SecurityLockEntity ent = null; return IsLocked(entityName, key, out ent); } public bool IsLocked(string entityName, string key, out SecurityLockEntity ent) { //find active lock ent = FindActiveLock(entityName, key); if (ent != null) return true; return false; } private SecurityLockEntity FindActiveLock(string entityName, string key) { SecurityLockEntity entLock = FindSingleByEntityNameAndPkeyId(entityName, key); if (entLock == null) return null; DateTime effectiveDt = DateTime.Now.AddMinutes(ACTIVE_WINDOW_MINUTES); if (entLock.CreatedDatetime.CompareTo(effectiveDt) < 0) { //it's an expired lock, remove it repository.DeleteNow(entLock); return null; } //it's an effective one return entLock; } private SecurityLockEntity FindSingleByEntityNameAndPkeyId(string entityName, string key) { SecurityLockEntity[] entLocks = FindByEntityNameAndPkeyId(entityName, key); if (entLocks == null || entLocks.Length <= 0) return null; return entLocks[0]; } private SecurityLockEntity[] FindByEntityNameAndPkeyId(string entityName, string key) { SecurityLockEntity[] entLocks = repository.DetachedEntities.Where(x => 1 == 1 && x.EntityName == entityName && x.PkeyId == key ).OrderByDescending(x => x.CreatedDatetime).ToArray(); if (entLocks == null && entLocks.Length <= 0) return null; return entLocks; } [AllowAnonymous] public void Lock(string entityName, string key, string username) { Lock(entityName, key, username, false); } private void Lock(string entityName, string key, string username, bool forceLock) { if (!forceLock) { SecurityLockEntity actLock = FindActiveLock(entityName, key); if (actLock != null && !actLock.LockedByActor.Equals(username)) { //"{0} [{1}] is locked by [{2}] on [{3}]"; //throw new ValidatedException(entityName + " [" + key + "] is locked by [" + actLock.LockedByActor + "] on [" + actLock.CreatedDatetime + "]"); ISecurityUserService securityLockService = service.ServiceProvider.GetService()!; var user = securityLockService.FindByStaffId(actLock.LockedByActor); string lockedByActor = actLock.LockedByActor; if (user != null) lockedByActor = user.UserLocalName; throw new ValidatedException(securityMessagePolicy.ComposeMessage(SecurityMessagePolicy.LOCKED, localeMessageManager.ComposeMessage(typeof(SecurityLockService).Name, entityName), key, lockedByActor, actLock.CreatedDatetime.GetFormattedDateString("yyyy-MM-dd HH:mm:ss"))); } } ClearLock(entityName, key); SecurityLockEntity newLock = new SecurityLockEntity(); newLock.EntityName = entityName; newLock.PkeyId = key; newLock.LockedByActor = username; Insert(newLock); } [AllowAnonymous] public void ForceLock(string entityName, string key, string username) { Lock(entityName, key, username, true); } [AllowAnonymous] public void ConsumeLock(string entityName, string key, string username) { SecurityLockEntity actLock = FindActiveLock(entityName, key); if (actLock == null) return; if (!actLock.LockedByActor.Equals(username)) { ISecurityUserService securityLockService = service.ServiceProvider.GetService()!; var user = securityLockService.FindByStaffId(actLock.LockedByActor); string lockedByActor = actLock.LockedByActor; if (user != null) lockedByActor = user.UserLocalName; throw new ValidatedException(securityMessagePolicy.ComposeMessage(SecurityMessagePolicy.EXPIRED, entityName, key, lockedByActor, actLock.CreatedDatetime)); //throw new ValidatedException("Your lock is expired or force removed. " + entityName + " [" + key + "] is locked by [" + actLock.LockedByActor + "] on [" + actLock.CreatedDatetime + "]"); } ClearLock(entityName, key); } [AllowAnonymous] public void ClearLock(string entityName, string key) { //find all the locks, then purge SecurityLockEntity[] entities = FindByEntityNameAndPkeyId(entityName, key); if (entities == null || entities.Length <= 0) return; repository.DeleteNow(entities); } } }