CloudBuilder/CloudBuilder.Security.Service/SecurityLockService.auto.cs
owenchen 1c0c83af5f ow
2026-05-12 10:09:31 +08:00

221 lines
8.8 KiB
C#

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<SecurityLockEntity> repository;
public SecurityLockService(IApplicationService service, ILocaleMessageManager localeMessageManager, IContext context)
{
this.service = service;
this.localeMessageManager = localeMessageManager;
this.context = context;
repository = service.GetRepository<IRepository<SecurityLockEntity>>();
securityMessagePolicy = service.ServiceProvider.GetRequiredKeyedService<SecurityMessagePolicy>(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<ISecurityUserService>()!;
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<ISecurityUserService>()!;
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);
}
}
}