Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions XCode/Membership/DataScopeContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ public class DataScopeContext
// 全部权限不需要缓存
if (scope == DataScopes.全部) return null;

var key = $"DataScope:{userId}:{(Int32)scope}";
// 缓存键包含部门编号,用户调岗(DepartmentID 变化)后立即使用新的部门列表,无需等待过期
var key = $"DataScope:{userId}:{deptId}:{(Int32)scope}";
return _cache.GetOrAdd(key, k => DataScopeHelper.GetAccessibleDepartmentIds(deptId, roles, scope));
}

Expand All @@ -120,10 +121,12 @@ public static void ClearCache(Int32 userId = 0)
{
if (userId > 0)
{
// 清除该用户所有范围的缓存
for (var i = 0; i <= 4; i++)
// 清除该用户所有缓存键(同时兼容旧键 DataScope:{userId}:{scope} 与新键 DataScope:{userId}:{deptId}:{scope})
// 缓存失效为尽力而为:快照期间并发新增的键可能被漏删,但其值本身就是最新计算结果,漏删无害
var prefix = $"DataScope:{userId}:";
foreach (var key in _cache.Keys.ToArray())
{
_cache.Remove($"DataScope:{userId}:{i}");
if (key.StartsWith(prefix)) _cache.Remove(key);
}
}
else
Expand Down Expand Up @@ -310,6 +313,8 @@ public static Int32[] ParseDepartmentIds(String? departmentIds)

if (typeof(IUserScope).IsAssignableFrom(type))
{
// 纯用户实体(无部门列,如日志):非全部权限始终按当前用户过滤。
// 无部门列时,本部门/自定义等范围不会 join 用户表放大成同事数据,避免越权可见。
var userField = GetUserField(factory);
if (userField is not null)
return userField.Equal(context.UserId);
Expand All @@ -319,7 +324,7 @@ public static Int32[] ParseDepartmentIds(String? departmentIds)
{
var deptField = GetDepartmentField(factory);
if (deptField is not null)
return BuildDepartmentFilter(context, deptField);
return BuildDepartmentScopeFilter(context, deptField);
}

return null;
Expand Down Expand Up @@ -371,6 +376,24 @@ public static Int32[] ParseDepartmentIds(String? departmentIds)

return deptField.In(deptIds);
}

/// <summary>构建纯部门实体的过滤表达式</summary>
/// <remarks>
/// 适用于“一行一个部门”的表(如部门表,DepartmentId 映射到主键 ID)。
/// 仅本人时没有可访问部门列表,退化为“当前用户所在部门”,即 ID=当前用户.DepartmentID,
/// 而不是恒假条件 Equal(-1) 导致空表。其余数据范围与普通部门过滤保持一致。
/// </remarks>
private static Expression? BuildDepartmentScopeFilter(DataScopeContext context, FieldItem? deptField)
{
if (deptField is null) return null;

// 仅本人:部门表退化为“当前用户所在部门”,避免恒假条件导致空表
if (context.DataScope == DataScopes.仅本人)
return deptField.Equal(context.DepartmentId);

// 其余范围(本部门/本部门及下级/自定义)与普通部门过滤语义一致,空数组仍为恒假条件
return BuildDepartmentFilter(context, deptField);
}
#endregion

#region 权限校验
Expand Down Expand Up @@ -431,6 +454,10 @@ public static Boolean CanAccess(IDepartmentScope entity, DataScopeContext? conte
// 全部权限可访问所有数据
if (context.DataScope == DataScopes.全部) return true;

// 仅本人:部门表“一行一个部门”,退化为“当前用户所在部门”
if (context.DataScope == DataScopes.仅本人)
return entity.DepartmentId == context.DepartmentId;

var deptIds = context.AccessibleDepartmentIds;
if (deptIds == null) return true;

Expand Down
4 changes: 3 additions & 1 deletion XCode/Membership/DataScopeModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,10 @@ protected override Boolean OnValid(IEntity entity, DataMethod method)
}
catch (InvalidOperationException ex)
{
// 失败时记录审计日志
// 失败时记录审计日志,并拒绝该操作(返回 false),避免越权写入
LogProvider.Provider.WriteLog(entity.GetType(), method + "", false, ex.Message);

return false;
}

return true;
Expand Down
14 changes: 13 additions & 1 deletion XCode/Membership/日志.Biz.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace XCode.Membership;

/// <summary>日志</summary>
public partial class Log : Entity<Log>
public partial class Log : Entity<Log>, IUserScope, IDataScopeFieldProvider
{
#region 对象操作
static Log()
Expand All @@ -17,6 +17,7 @@ static Log()
Meta.Interceptors.Add<UserInterceptor>();
Meta.Interceptors.Add<IPInterceptor>();
Meta.Interceptors.Add<TraceInterceptor>();
Meta.Interceptors.Add<DataScopeInterceptor>();

#if !DEBUG
// 关闭SQL日志
Expand Down Expand Up @@ -232,4 +233,15 @@ public static IList<Log> FindAllByCreateUserID(Int32 createUserId)
/// <returns></returns>
public override String ToString() => $"{Category} {Action} {UserName} {CreateTime:yyyy-MM-dd HH:mm:ss} {Remark}";
#endregion

#region IUserScope 成员
// 日志表没有部门列,仅按创建用户过滤;本部门/自定义等范围不会 join 用户表放大成同事数据
Int32 IUserScope.UserId { get => CreateUserID; set => CreateUserID = value; }
#endregion

#region IDataScopeFieldProvider 成员
XCode.Configuration.FieldItem? IDataScopeFieldProvider.GetUserField() => _.CreateUserID;
XCode.Configuration.FieldItem? IDataScopeFieldProvider.GetDepartmentField() => null;
XCode.Configuration.FieldItem? IDataScopeFieldProvider.GetTenantField() => null;
#endregion
}
1 change: 1 addition & 0 deletions XCode/Membership/用户.Biz.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ static User()
Meta.Interceptors.Add<UserInterceptor>();
Meta.Interceptors.Add<TimeInterceptor>();
Meta.Interceptors.Add<IPInterceptor>();
Meta.Interceptors.Add<DataScopeInterceptor>();
}

/// <summary>首次连接数据库时初始化数据,仅用于实体类重载,用户不应该调用该方法</summary>
Expand Down
5 changes: 5 additions & 0 deletions XCode/Membership/菜单.Biz.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ public override Boolean Valid(DataMethod method)

if (Icon == "&#xe63f;") Icon = null;

// 数据范围默认 -1(使用角色默认)。列默认值为 -1,但枚举默认是 0=全部,
// 新建或未显式配置的菜单若保存为 0 会覆盖角色默认为“全部”,故此处保持 -1。
if (method == DataMethod.Insert && !IsDirty(__.DataScope) && (Int32)DataScope == 0)
DataScope = (DataScopes)(-1);

SavePermission();

return base.Valid(method);
Expand Down
4 changes: 3 additions & 1 deletion XCode/Membership/角色.Biz.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,9 @@ public override Boolean Valid(DataMethod method)
Type = IsSystem ? RoleTypes.系统 : RoleTypes.普通;
}

if (DataScope == 0)
// 仅在新增且未显式设置数据范围时,按角色类型填充默认值。
// DataScopes.全部==0,更新或已显式赋值时,0 就表示“全部”,不再改写。
if (method == DataMethod.Insert && !IsDirty(__.DataScope) && DataScope == 0)
{
DataScope = Type switch
{
Expand Down
13 changes: 12 additions & 1 deletion XCode/Membership/部门.Biz.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
namespace XCode.Membership;

/// <summary>部门。组织机构,多级树状结构</summary>
public partial class Department : Entity<Department>, ITenantScope
public partial class Department : Entity<Department>, ITenantScope, IDepartmentScope, IDataScopeFieldProvider
{
#region 对象操作
private static Int32 MaxCacheCount = 10000;
Expand All @@ -28,6 +28,7 @@ static Department()
Meta.Interceptors.Add<TimeInterceptor>();
Meta.Interceptors.Add<IPInterceptor>();
Meta.Interceptors.Add<TenantInterceptor>();
Meta.Interceptors.Add<DataScopeInterceptor>();
}

/// <summary>验证并修补数据,返回验证结果,或者通过抛出异常的方式提示验证失败。</summary>
Expand Down Expand Up @@ -261,4 +262,14 @@ public static IList<Department> Search(Int32 tenantId, DepartmentTypes type, Int

#region 业务操作
#endregion

#region IDepartmentScope 成员
Int32 IDepartmentScope.DepartmentId { get => ID; set => ID = value; }
#endregion

#region IDataScopeFieldProvider 成员
XCode.Configuration.FieldItem? IDataScopeFieldProvider.GetUserField() => null;
XCode.Configuration.FieldItem? IDataScopeFieldProvider.GetDepartmentField() => _.ID;
XCode.Configuration.FieldItem? IDataScopeFieldProvider.GetTenantField() => _.TenantId;
#endregion
}
169 changes: 168 additions & 1 deletion XUnitTest.XCode/Membership/DataScopeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@ public void DataScopeInterceptor_OnValid_Update_NoDirty_SkipsValidation()
DataScope = DataScopes.仅本人
};
var entity = new DataScopeTestEntity { UserId = 999, DepartmentId = 888 };
// 不修改任何属性,所以没有脏数据
// 清除脏数据,模拟没有任何字段被修改的更新场景
((IEntity)entity).Dirtys.Clear();

// Act
var result = module.Valid(entity, DataMethod.Update);
Expand Down Expand Up @@ -1390,6 +1391,172 @@ public void DataScopes_EnumValues_Correct()
Assert.Equal(4, (Int32)DataScopes.自定义);
}
#endregion

#region 行权事实源验收测试
[Fact]
[DisplayName("角色_普通角色全部_Update后仍为全部")]
public void Role_NormalRole_ScopeAll_Update_StaysAll()
{
// Arrange 普通角色显式使用“全部(0)”
var role = new Role { Name = "普通角色", Type = RoleTypes.普通, DataScope = DataScopes.全部 };

// Act 更新时不应把 0 改写成“本部门”
role.Valid(DataMethod.Update);

// Assert
Assert.Equal(DataScopes.全部, role.DataScope);
}

[Fact]
[DisplayName("角色_Insert未赋DataScope时填充Type默认值")]
public void Role_Insert_WithoutDataScope_FillsTypeDefault()
{
// Arrange 未显式赋值 DataScope
var role = new Role { Name = "业务角色", Type = RoleTypes.普通 };

// Act
role.Valid(DataMethod.Insert);

// Assert 普通角色默认本部门
Assert.Equal(DataScopes.本部门, role.DataScope);
}

[Fact]
[DisplayName("拦截器_仅本人_拒绝修改他人IDataScope数据")]
public void Interceptor_SelfOnly_RejectUpdateOthersData()
{
// Arrange
var module = new DataScopeInterceptor();
DataScopeContext.Current = new DataScopeContext
{
UserId = 100,
DepartmentId = 200,
DataScope = DataScopes.仅本人
};
var entity = new DataScopeTestEntity { UserId = 999, DepartmentId = 888 };
entity.Name = "改动"; // 制造脏数据

// Act
var result = module.Valid(entity, DataMethod.Update);

// Assert 校验失败不再放行
Assert.False(result);
}

[Fact]
[DisplayName("拦截器_本部门_拒绝在越权部门插入")]
public void Interceptor_Department_RejectInsertForeignDept()
{
// Arrange
var module = new DataScopeInterceptor();
DataScopeContext.Current = new DataScopeContext
{
UserId = 100,
DepartmentId = 200,
DataScope = DataScopes.本部门,
AccessibleDepartmentIds = [200]
};
var entity = new DepartmentScopeTestEntity { DepartmentId = 999 };

// Act
var result = module.Valid(entity, DataMethod.Insert);

// Assert
Assert.False(result);
}

[Fact]
[DisplayName("User_本部门滤DepartmentID_仅本人滤ID")]
public void User_Filter_Department_And_SelfOnly()
{
// 本部门:按部门过滤
DataScopeContext.Current = new DataScopeContext
{
UserId = 100,
DepartmentId = 200,
DataScope = DataScopes.本部门,
AccessibleDepartmentIds = [200]
};
var deptFilter = DataScopeHelper.GetFilter<User>();
Assert.NotNull(deptFilter);
var deptSql = deptFilter!.ToString();
Assert.Contains("DepartmentID", deptSql);
Assert.Contains("200", deptSql);

// 仅本人:按用户主键 ID 过滤
DataScopeContext.Current = new DataScopeContext
{
UserId = 100,
DepartmentId = 200,
DataScope = DataScopes.仅本人
};
var selfFilter = DataScopeHelper.GetFilter<User>();
Assert.NotNull(selfFilter);
var selfSql = selfFilter!.ToString();
Assert.DoesNotContain("DepartmentID", selfSql);
Assert.Contains("100", selfSql);
}

[Fact]
[DisplayName("Log_本部门仍按CreateUserID过滤")]
public void Log_Filter_Department_UsesCreateUserID()
{
// 日志表无部门列,本部门范围仍按创建用户过滤,不放大成同事数据
DataScopeContext.Current = new DataScopeContext
{
UserId = 100,
DepartmentId = 200,
DataScope = DataScopes.本部门,
AccessibleDepartmentIds = [200]
};
var filter = DataScopeHelper.GetFilter<Log>();
Assert.NotNull(filter);
var sql = filter!.ToString();
Assert.Contains("CreateUserID", sql);
Assert.Contains("100", sql);
}

[Fact]
[DisplayName("Department_仅本人滤当前部门ID而非-1")]
public void Department_Filter_SelfOnly_UsesCurrentDeptId()
{
// 部门表“一行一个部门”,仅本人退化为 ID=当前用户所在部门,而不是恒假条件 -1
DataScopeContext.Current = new DataScopeContext
{
UserId = 100,
DepartmentId = 200,
DataScope = DataScopes.仅本人
};
var filter = DataScopeHelper.GetFilter<Department>();
Assert.NotNull(filter);
var sql = filter!.ToString();
Assert.Contains("200", sql);
Assert.DoesNotContain("-1", sql);
}

[Fact]
[DisplayName("缓存_更换部门后可访问部门列表立即更新")]
public void Cache_DepartmentChange_UpdatesImmediately()
{
// Arrange
DataScopeContext.ClearCache();
var user = new MockUser { ID = 100, DepartmentID = 200 };
var role = new MockRole { ID = 1, IsSystem = false, DataScope = DataScopes.本部门 };
user.Role = role;
user.Roles = [role];

// Act 初次创建
var ctx1 = DataScopeContext.Create(user);
Assert.NotNull(ctx1);
Assert.Equal([200], ctx1!.AccessibleDepartmentIds);

// 调岗后再次创建,缓存键含部门编号,立即生效
user.DepartmentID = 300;
var ctx2 = DataScopeContext.Create(user);
Assert.NotNull(ctx2);
Assert.Equal([300], ctx2!.AccessibleDepartmentIds);
}
#endregion
}

#region 测试用实体类
Expand Down