diff --git a/docs/en/Modules/Identity.md b/docs/en/Modules/Identity.md index b950658505..ac86b13564 100644 --- a/docs/en/Modules/Identity.md +++ b/docs/en/Modules/Identity.md @@ -4,3 +4,26 @@ Identity module is used to manage [organization units](Organization-Units.md), r See [the source code](https://github.com/abpframework/abp/tree/dev/modules/identity). Documentation will come soon... + +## Identity Security Log + +The security log can record some important operations or changes about your account. You can save the security log if needed. + +You can inject and use `IdentitySecurityLogManager` or `ISecurityLogManager` to write security logs. It will create a log object by default and fill in some common values, such as `CreationTime`, `ClientIpAddress`, `BrowserInfo`, `current user/tenant`, etc. Of course, you can override them. + +```cs +await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() +{ + Identity = "IdentityServer"; + Action = "ChangePassword"; +}); +``` + +Configure `AbpSecurityLogOptions` to provide the application name for the log or disable this feature. **Enabled** by default. + +```cs +Configure(options => +{ + options.ApplicationName = "AbpSecurityTest"; +}); +``` diff --git a/docs/zh-Hans/Modules/Identity.md b/docs/zh-Hans/Modules/Identity.md index f396de00c1..5589fb367e 100644 --- a/docs/zh-Hans/Modules/Identity.md +++ b/docs/zh-Hans/Modules/Identity.md @@ -1,5 +1,28 @@ # 身份管理模块 -身份模块基于Microsoft Identity 库用于管理[组织单元](Organization-Units.md), 角色, 用户和他们的权限. +身份模块基于Microsoft Identity库用于管理[组织单元](Organization-Units.md), 角色, 用户和他们的权限. -参阅 [源码](https://github.com/abpframework/abp/tree/dev/modules/identity). 文档很快会被完善. \ No newline at end of file +参阅 [源码](https://github.com/abpframework/abp/tree/dev/modules/identity). 文档很快会被完善. + +## Identity安全日志 + +安全日志可以记录账户的一些重要的操作或者改动, 你可以在在一些功能中保存安全日志. + +你可以注入和使用 `IdentitySecurityLogManager` 或 `ISecurityLogManager` 来保存安全日志. 默认它会创建一个安全日志对象并填充常用的值. 如 `CreationTime`, `ClientIpAddress`, `BrowserInfo`, `current user/tenant`等等. 当然你可以自定义这些值. + +```cs +await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() +{ + Identity = "IdentityServer"; + Action = "ChangePassword"; +}); +``` + +通过配置 `AbpSecurityLogOptions` 来提供应用程序的名称或者禁用安全日志功能. 默认是**启用**状态. + +```cs +Configure(options => +{ + options.ApplicationName = "AbpSecurityTest"; +}); +``` diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs index bf39874e00..0a69e90eae 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs @@ -28,9 +28,11 @@ namespace Volo.Abp.Identity ); Task> GetListAsync( + Guid? parentId, string sorting = null, int maxResultCount = int.MaxValue, int skipCount = 0, + string filter = null, bool includeDetails = false, CancellationToken cancellationToken = default ); @@ -112,5 +114,10 @@ namespace Volo.Abp.Identity OrganizationUnit organizationUnit, CancellationToken cancellationToken = default ); + + Task GetLongCountAsync( + Guid? parentId, + string filter = null, + CancellationToken cancellationToken = default); } } diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs index 59fbd9518e..5b5ad8a6cf 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; +using System.IO; using System.Linq.Dynamic.Core; using System.Linq; using System.Threading; @@ -44,14 +45,20 @@ namespace Volo.Abp.Identity.EntityFrameworkCore } public virtual async Task> GetListAsync( + Guid? parentId, string sorting = null, int maxResultCount = int.MaxValue, int skipCount = 0, + string filter = null, bool includeDetails = true, CancellationToken cancellationToken = default) { return await DbSet .IncludeDetails(includeDetails) + .Where(ou=>ou.ParentId==parentId) + .WhereIf(!filter.IsNullOrWhiteSpace(), + ou => ou.DisplayName.Contains(filter) || + ou.Code.Contains(filter)) .OrderBy(sorting ?? nameof(OrganizationUnit.DisplayName)) .PageBy(skipCount, maxResultCount) .ToListAsync(GetCancellationToken(cancellationToken)); @@ -90,9 +97,9 @@ namespace Volo.Abp.Identity.EntityFrameworkCore CancellationToken cancellationToken = default) { var query = from organizationRole in DbContext.Set() - join role in DbContext.Roles.IncludeDetails(includeDetails) on organizationRole.RoleId equals role.Id - where organizationRole.OrganizationUnitId == organizationUnit.Id - select role; + join role in DbContext.Roles.IncludeDetails(includeDetails) on organizationRole.RoleId equals role.Id + where organizationRole.OrganizationUnitId == organizationUnit.Id + select role; query = query .OrderBy(sorting ?? nameof(IdentityRole.Name)) .PageBy(skipCount, maxResultCount); @@ -105,9 +112,9 @@ namespace Volo.Abp.Identity.EntityFrameworkCore CancellationToken cancellationToken = default) { var query = from organizationRole in DbContext.Set() - join role in DbContext.Roles on organizationRole.RoleId equals role.Id - where organizationRole.OrganizationUnitId == organizationUnit.Id - select role; + join role in DbContext.Roles on organizationRole.RoleId equals role.Id + where organizationRole.OrganizationUnitId == organizationUnit.Id + select role; return await query.CountAsync(GetCancellationToken(cancellationToken)); } @@ -157,8 +164,8 @@ namespace Volo.Abp.Identity.EntityFrameworkCore var query = CreateGetMembersFilteredQuery(organizationUnit, filter); return await query.IncludeDetails(includeDetails).OrderBy(sorting ?? nameof(IdentityUser.UserName)) - .PageBy(skipCount, maxResultCount) - .ToListAsync(GetCancellationToken(cancellationToken)); + .PageBy(skipCount, maxResultCount) + .ToListAsync(GetCancellationToken(cancellationToken)); } public virtual async Task GetMembersCountAsync( @@ -245,7 +252,19 @@ namespace Volo.Abp.Identity.EntityFrameworkCore DbContext.Set().RemoveRange(ouMembersQuery); } - protected virtual IQueryable CreateGetMembersFilteredQuery(OrganizationUnit organizationUnit, string filter = null) + public virtual async Task GetLongCountAsync(Guid? parentId, string filter = null, + CancellationToken cancellationToken = default) + { + return await DbSet + .Where(ou=>ou.ParentId==parentId) + .WhereIf(!filter.IsNullOrWhiteSpace(), ou => + ou.DisplayName.Contains(filter) || + ou.Code.Contains(filter)) + .LongCountAsync(GetCancellationToken(cancellationToken)); + } + + protected virtual IQueryable CreateGetMembersFilteredQuery(OrganizationUnit organizationUnit, + string filter = null) { var query = from userOu in DbContext.Set() join user in DbContext.Users on userOu.UserId equals user.Id @@ -264,4 +283,4 @@ namespace Volo.Abp.Identity.EntityFrameworkCore return query; } } -} +} \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs index 10ae4cd038..f6ae630eda 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs @@ -16,7 +16,7 @@ namespace Volo.Abp.Identity.MongoDB { public class MongoOrganizationUnitRepository : MongoDbRepository, - IOrganizationUnitRepository + IOrganizationUnitRepository { public MongoOrganizationUnitRepository( IMongoDbContextProvider dbContextProvider) @@ -41,8 +41,8 @@ namespace Volo.Abp.Identity.MongoDB CancellationToken cancellationToken = default) { return await GetMongoQueryable() - .Where(ou => ou.Code.StartsWith(code) && ou.Id != parentId.Value) - .ToListAsync(GetCancellationToken(cancellationToken)); + .Where(ou => ou.Code.StartsWith(code) && ou.Id != parentId.Value) + .ToListAsync(GetCancellationToken(cancellationToken)); } public virtual async Task> GetListAsync( @@ -51,22 +51,28 @@ namespace Volo.Abp.Identity.MongoDB CancellationToken cancellationToken = default) { return await GetMongoQueryable() - .Where(t => ids.Contains(t.Id)) - .ToListAsync(GetCancellationToken(cancellationToken)); + .Where(t => ids.Contains(t.Id)) + .ToListAsync(GetCancellationToken(cancellationToken)); } public virtual async Task> GetListAsync( + Guid? parentId, string sorting = null, int maxResultCount = int.MaxValue, int skipCount = 0, + string filter = null, bool includeDetails = false, CancellationToken cancellationToken = default) { return await GetMongoQueryable() - .OrderBy(sorting ?? nameof(OrganizationUnit.DisplayName)) - .As>() - .PageBy>(skipCount, maxResultCount) - .ToListAsync(GetCancellationToken(cancellationToken)); + .Where(ou=>ou.ParentId==parentId) + .WhereIf(!filter.IsNullOrWhiteSpace(), + ou => ou.DisplayName.Contains(filter) || + ou.Code.Contains(filter)) + .OrderBy(sorting ?? nameof(OrganizationUnit.DisplayName)) + .As>() + .PageBy>(skipCount, maxResultCount) + .ToListAsync(GetCancellationToken(cancellationToken)); } public virtual async Task GetAsync( @@ -166,7 +172,6 @@ namespace Volo.Abp.Identity.MongoDB return await query.CountAsync(GetCancellationToken(cancellationToken)); } - public async Task> GetUnaddedUsersAsync( OrganizationUnit organizationUnit, string sorting = null, @@ -213,7 +218,8 @@ namespace Volo.Abp.Identity.MongoDB return Task.FromResult(0); } - public virtual async Task RemoveAllMembersAsync(OrganizationUnit organizationUnit, CancellationToken cancellationToken = default) + public virtual async Task RemoveAllMembersAsync(OrganizationUnit organizationUnit, + CancellationToken cancellationToken = default) { var users = await DbContext.Users.AsQueryable() .Where(u => u.OrganizationUnits.Any(uou => uou.OrganizationUnitId == organizationUnit.Id)) @@ -227,7 +233,19 @@ namespace Volo.Abp.Identity.MongoDB } } - protected virtual IMongoQueryable CreateGetMembersFilteredQuery(OrganizationUnit organizationUnit, string filter = null) + public virtual async Task GetLongCountAsync(Guid? parentId, string filter = null, + CancellationToken cancellationToken = default) + { + return await GetMongoQueryable() + .Where(ou=>ou.ParentId==parentId) + .WhereIf>(!filter.IsNullOrWhiteSpace(), ou => + ou.DisplayName.Contains(filter) || + ou.Code.Contains(filter)) + .LongCountAsync(GetCancellationToken(cancellationToken)); + } + + protected virtual IMongoQueryable CreateGetMembersFilteredQuery(OrganizationUnit organizationUnit, + string filter = null) { return DbContext.Users.AsQueryable() .Where(u => u.OrganizationUnits.Any(uou => uou.OrganizationUnitId == organizationUnit.Id)) @@ -240,4 +258,4 @@ namespace Volo.Abp.Identity.MongoDB ); } } -} +} \ No newline at end of file diff --git a/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityRoleAppService_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityRoleAppService_Tests.cs index 5d64bb6c78..b32c830002 100644 --- a/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityRoleAppService_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityRoleAppService_Tests.cs @@ -11,11 +11,14 @@ namespace Volo.Abp.Identity { private readonly IIdentityRoleAppService _roleAppService; private readonly IIdentityRoleRepository _roleRepository; - + private readonly IOrganizationUnitRepository _organizationUnitRepository; + private readonly OrganizationUnitManager _organization; public IdentityRoleAppService_Tests() { _roleAppService = GetRequiredService(); _roleRepository = GetRequiredService(); + _organization = GetRequiredService(); + _organizationUnitRepository=GetRequiredService(); } [Fact] @@ -81,6 +84,31 @@ namespace Volo.Abp.Identity role.Name.ShouldBe(input.Name); } + [Fact] + public async Task CreateWithDetailsAsync() + { + //Arrange + var input = new IdentityRoleCreateDto + { + Name = Guid.NewGuid().ToString("N").Left(8) + }; + + var orgInput=new OrganizationUnit( + _organization.GuidGenerator.Create(), + Guid.NewGuid().ToString("N").Left(8) + ); + + //Act + var result = await _roleAppService.CreateAsync(input); + + await _organization.CreateAsync(orgInput); + + var role = await _roleRepository.GetAsync(result.Id); + await _organization.AddRoleToOrganizationUnitAsync(role,orgInput); + //Assert + orgInput.Roles.Count.ShouldBeGreaterThan(0); + } + [Fact] public async Task UpdateAsync() { diff --git a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitRepository_Tests.cs b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitRepository_Tests.cs index a671ba3284..d63daf1646 100644 --- a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitRepository_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitRepository_Tests.cs @@ -59,6 +59,22 @@ namespace Volo.Abp.Identity var ous = await _organizationUnitRepository.GetListAsync(ouIds); ous.Count.ShouldBe(2); ous.ShouldContain(ou => ou.Id == ouIds.First()); + + var ou11 = await _organizationUnitRepository.GetAsync("OU11"); + ou11.ShouldNotBeNull(); + var ou11Children = await _organizationUnitRepository.GetListAsync(ou11.Id, includeDetails: true); + ou11Children.Count.ShouldBe(2); + } + + [Fact] + public async Task GetLongCountAsync() + { + (await _organizationUnitRepository.GetLongCountAsync(_guidGenerator.Create(), filter: "11")).ShouldBe(0); + var countRoot = await _organizationUnitRepository.GetLongCountAsync(null, filter: "1"); + countRoot.ShouldBe(1); + var ou11 = await _organizationUnitRepository.GetAsync("OU11"); + ou11.ShouldNotBeNull(); + (await _organizationUnitRepository.GetLongCountAsync(ou11.Id, "2")).ShouldBe(1); } [Fact] @@ -192,7 +208,7 @@ namespace Volo.Abp.Identity { OrganizationUnit ou1 = await _organizationUnitRepository.GetAsync("OU111", true); OrganizationUnit ou2 = await _organizationUnitRepository.GetAsync("OU112", true); - var users = await _identityUserRepository.GetUsersInOrganizationsListAsync(new List {ou1.Id, ou2.Id}); + var users = await _identityUserRepository.GetUsersInOrganizationsListAsync(new List { ou1.Id, ou2.Id }); users.Count.ShouldBeGreaterThan(0); } diff --git a/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.html b/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.html index b7c777a449..b98aa7e4ff 100644 --- a/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.html +++ b/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.html @@ -7,6 +7,7 @@ [nzData]="nodes" [nzTreeTemplate]="treeTemplate" [nzExpandedKeys]="expandedKeys" + [nzExpandedIcon]="expandedIconTemplate?.template" (nzExpandChange)="onExpandedKeysChange($event)" (nzCheckBoxChange)="onCheckboxChange($event)" (nzOnDrop)="onDrop($event)" @@ -18,7 +19,16 @@ [title]="node.title" (click)="onSelectedNodeChange(node)" > - {{ node.title }} + + + + {{ node.title }} +
diff --git a/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.ts b/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.ts index ac7a709048..7ba6280c0b 100644 --- a/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.ts +++ b/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.ts @@ -9,6 +9,8 @@ import { } from '@angular/core'; import { NzFormatEmitEvent, NzFormatBeforeDropEvent } from 'ng-zorro-antd/tree'; import { of } from 'rxjs'; +import { TreeNodeTemplateDirective } from '../templates/tree-node-template.directive'; +import { ExpandedIconTemplateDirective } from '../templates/expanded-icon-template.directive'; export type DropEvent = NzFormatEmitEvent & { pos: number }; @@ -25,6 +27,8 @@ export class TreeComponent { dropPosition: number; @ContentChild('menu') menu: TemplateRef; + @ContentChild(TreeNodeTemplateDirective) customNodeTemplate: TreeNodeTemplateDirective; + @ContentChild(ExpandedIconTemplateDirective) expandedIconTemplate: ExpandedIconTemplateDirective; @Output() readonly checkedKeysChange = new EventEmitter(); @Output() readonly expandedKeysChange = new EventEmitter(); @Output() readonly selectedNodeChange = new EventEmitter(); diff --git a/npm/ng-packs/packages/components/tree/src/lib/templates/expanded-icon-template.directive.ts b/npm/ng-packs/packages/components/tree/src/lib/templates/expanded-icon-template.directive.ts new file mode 100644 index 0000000000..35706bd25f --- /dev/null +++ b/npm/ng-packs/packages/components/tree/src/lib/templates/expanded-icon-template.directive.ts @@ -0,0 +1,8 @@ +import { Directive, TemplateRef } from '@angular/core'; + +@Directive({ + selector: '[abpTreeExpandedIconTemplate],[abp-tree-expanded-icon-template]', +}) +export class ExpandedIconTemplateDirective { + constructor(public template: TemplateRef) {} +} diff --git a/npm/ng-packs/packages/components/tree/src/lib/templates/tree-node-template.directive.ts b/npm/ng-packs/packages/components/tree/src/lib/templates/tree-node-template.directive.ts new file mode 100644 index 0000000000..56c66af04b --- /dev/null +++ b/npm/ng-packs/packages/components/tree/src/lib/templates/tree-node-template.directive.ts @@ -0,0 +1,8 @@ +import { Directive, TemplateRef } from '@angular/core'; + +@Directive({ + selector: '[abpTreeNodeTemplate],[abp-tree-node-template]', +}) +export class TreeNodeTemplateDirective { + constructor(public template: TemplateRef) {} +} diff --git a/npm/ng-packs/packages/components/tree/src/lib/tree.module.ts b/npm/ng-packs/packages/components/tree/src/lib/tree.module.ts index f16ed0dd55..b645a69925 100644 --- a/npm/ng-packs/packages/components/tree/src/lib/tree.module.ts +++ b/npm/ng-packs/packages/components/tree/src/lib/tree.module.ts @@ -1,12 +1,18 @@ +import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; import { NzTreeModule } from 'ng-zorro-antd/tree'; import { TreeComponent } from './components/tree.component'; -import { CommonModule } from '@angular/common'; +import { TreeNodeTemplateDirective } from './templates/tree-node-template.directive'; +import { ExpandedIconTemplateDirective } from './templates/expanded-icon-template.directive'; + +const templates = [TreeNodeTemplateDirective, ExpandedIconTemplateDirective]; + +const exported = [...templates, TreeComponent]; @NgModule({ imports: [CommonModule, NzTreeModule, NgbDropdownModule], - exports: [TreeComponent], - declarations: [TreeComponent], + exports: [...exported], + declarations: [...exported], }) export class TreeModule {} diff --git a/npm/ng-packs/packages/components/tree/src/public-api.ts b/npm/ng-packs/packages/components/tree/src/public-api.ts index df82ac0f93..1f3dd48e00 100644 --- a/npm/ng-packs/packages/components/tree/src/public-api.ts +++ b/npm/ng-packs/packages/components/tree/src/public-api.ts @@ -1,3 +1,5 @@ export * from './lib/tree.module'; export * from './lib/components/tree.component'; export * from './lib/utils/nz-tree-adapter'; +export * from './lib/templates/tree-node-template.directive'; +export * from './lib/templates/expanded-icon-template.directive'; diff --git a/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.ts b/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.ts index 4e1df0b294..98d5e4cbbc 100644 --- a/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.ts +++ b/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.ts @@ -3,7 +3,8 @@ import { LocaleDirection } from '@abp/ng.theme.shared'; import { Component, EventEmitter, Input, Output } from '@angular/core'; import { finalize } from 'rxjs/operators'; import { FeatureManagement } from '../../models/feature-management'; -import { FeatureDto, FeaturesService, UpdateFeatureDto } from '../../proxy/feature-management'; +import { FeaturesService } from '../../proxy/feature-management/features.service'; +import { FeatureDto, UpdateFeatureDto } from '../../proxy/feature-management/models'; enum ValueTypes { ToggleStringValueType = 'ToggleStringValueType', diff --git a/npm/ng-packs/packages/feature-management/src/lib/services/feature-management-state.service.ts b/npm/ng-packs/packages/feature-management/src/lib/services/feature-management-state.service.ts index 79d7fbef34..20c76c41ae 100644 --- a/npm/ng-packs/packages/feature-management/src/lib/services/feature-management-state.service.ts +++ b/npm/ng-packs/packages/feature-management/src/lib/services/feature-management-state.service.ts @@ -1,8 +1,7 @@ import { Injectable } from '@angular/core'; import { Store } from '@ngxs/store'; -import { FeatureManagementState } from '../states'; -import { FeatureManagement } from '../models'; -import { GetFeatures, UpdateFeatures } from '../actions'; +import { GetFeatures, UpdateFeatures } from '../actions/feature-management.actions'; +import { FeatureManagementState } from '../states/feature-management.state'; @Injectable({ providedIn: 'root',