diff --git a/docs/en/modules/tenant-management.md b/docs/en/modules/tenant-management.md index 101c7f076fe..b19f390a415 100644 --- a/docs/en/modules/tenant-management.md +++ b/docs/en/modules/tenant-management.md @@ -146,7 +146,7 @@ This section can be used as a reference if you want to [customize](../framework/ * `TenantAppService` -In addition to tenant CRUD operations, `ITenantAppService` provides `GetDefaultConnectionStringAsync`, `UpdateDefaultConnectionStringAsync` and `DeleteDefaultConnectionStringAsync`. The HTTP API exposes these operations as `GET`, `PUT` and `DELETE` on `/api/multi-tenancy/tenants/{id}/default-connection-string`; the `PUT` request receives `defaultConnectionString` as a query parameter. +In addition to tenant CRUD operations, `ITenantAppService` provides `GetDefaultConnectionStringAsync`, `UpdateDefaultConnectionStringAsync` and `DeleteDefaultConnectionStringAsync`. The HTTP API exposes these operations as `GET`, `PUT` and `DELETE` on `/api/multi-tenancy/tenants/{id}/default-connection-string`; the `PUT` request receives `defaultConnectionString` in the request body, as a JSON string with the `application/json` content type. #### Permissions diff --git a/framework/src/Volo.Abp.Auditing.Contracts/Volo/Abp/Auditing/DisableAuditingAttribute.cs b/framework/src/Volo.Abp.Auditing.Contracts/Volo/Abp/Auditing/DisableAuditingAttribute.cs index 1e270b643a6..e71b6f7b3e5 100644 --- a/framework/src/Volo.Abp.Auditing.Contracts/Volo/Abp/Auditing/DisableAuditingAttribute.cs +++ b/framework/src/Volo.Abp.Auditing.Contracts/Volo/Abp/Auditing/DisableAuditingAttribute.cs @@ -2,7 +2,7 @@ namespace Volo.Abp.Auditing; -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property)] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter)] public class DisableAuditingAttribute : Attribute { /// diff --git a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingHelper.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingHelper.cs index 028096d0ac6..8d354360626 100644 --- a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingHelper.cs +++ b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingHelper.cs @@ -178,7 +178,7 @@ public virtual AuditLogActionInfo CreateAuditLogAction( ? type.FullName! : "", MethodName = method.Name, - Parameters = SerializeConvertArguments(arguments), + Parameters = SerializeConvertArguments(method, arguments), ExecutionTime = Clock.Now }; @@ -220,6 +220,24 @@ protected virtual void ExecutePreContributors(AuditLogInfo auditLogInfo) } } + protected virtual string SerializeConvertArguments(MethodInfo method, IDictionary arguments) + { + var disabledParameters = method.GetParameters() + .Where(x => x.IsDefined(typeof(DisableAuditingAttribute), true)) + .Select(x => x.Name) + .ToArray(); + + if (disabledParameters.Any()) + { + arguments = arguments.ToDictionary( + x => x.Key, + x => disabledParameters.Contains(x.Key) ? null : x.Value + ); + } + + return SerializeConvertArguments(arguments); + } + protected virtual string SerializeConvertArguments(IDictionary arguments) { try diff --git a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AuditingHelper_Tests.cs b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AuditingHelper_Tests.cs index f1f82a4459c..09b192282db 100644 --- a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AuditingHelper_Tests.cs +++ b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AuditingHelper_Tests.cs @@ -1,7 +1,9 @@ +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using NSubstitute; +using Shouldly; using Volo.Abp.DependencyInjection; using Xunit; @@ -125,6 +127,20 @@ public void Should_Return_False_With_Nested_DisableAuditing() } } + [Fact] + public async Task Should_Not_Write_Parameter_Value_With_DisableAuditing() + { + var myAuditedObject = GetRequiredService(); + + await myAuditedObject.DoItWithSecretAsync("MyTenant", "Server=localhost;Password=1q2w3E*"); + + var auditLog = (AuditLogInfo)AuditingStore.ReceivedCalls().Last().GetArguments()[0]!; + var action = auditLog.Actions.Single(x => x.MethodName == nameof(MyAuditedObject.DoItWithSecretAsync)); + + action.Parameters.ShouldContain("MyTenant"); + action.Parameters.ShouldNotContain("1q2w3E*"); + } + public interface IMyAuditedObject : ITransientDependency, IAuditingEnabled { } @@ -135,5 +151,10 @@ public virtual Task DoItAsync() { return Task.CompletedTask; } + + public virtual Task DoItWithSecretAsync(string name, [DisableAuditing] string connectionString) + { + return Task.CompletedTask; + } } } diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Application.Contracts/Volo/Abp/TenantManagement/ITenantAppService.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Application.Contracts/Volo/Abp/TenantManagement/ITenantAppService.cs index 56d77a83967..edaae09c045 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Application.Contracts/Volo/Abp/TenantManagement/ITenantAppService.cs +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Application.Contracts/Volo/Abp/TenantManagement/ITenantAppService.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Volo.Abp.Application.Services; +using Volo.Abp.Auditing; namespace Volo.Abp.TenantManagement; @@ -8,7 +9,7 @@ public interface ITenantAppService : ICrudAppService GetDefaultConnectionStringAsync(Guid id); - Task UpdateDefaultConnectionStringAsync(Guid id, string defaultConnectionString); + Task UpdateDefaultConnectionStringAsync(Guid id, [DisableAuditing] string defaultConnectionString); Task DeleteDefaultConnectionStringAsync(Guid id); } diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Application/Volo/Abp/TenantManagement/TenantAppService.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Application/Volo/Abp/TenantManagement/TenantAppService.cs index e902482c438..3db5800c3a3 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Application/Volo/Abp/TenantManagement/TenantAppService.cs +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Application/Volo/Abp/TenantManagement/TenantAppService.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Volo.Abp.Application.Dtos; +using Volo.Abp.Auditing; using Volo.Abp.Data; using Volo.Abp.EventBus.Distributed; using Volo.Abp.EventBus.Local; @@ -133,7 +134,7 @@ public virtual async Task GetDefaultConnectionStringAsync(Guid id) } [Authorize(TenantManagementPermissions.Tenants.ManageConnectionStrings)] - public virtual async Task UpdateDefaultConnectionStringAsync(Guid id, string defaultConnectionString) + public virtual async Task UpdateDefaultConnectionStringAsync(Guid id, [DisableAuditing] string defaultConnectionString) { var tenant = await TenantRepository.GetAsync(id); if (tenant.FindDefaultConnectionString() != defaultConnectionString) diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/TenantConnectionString.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/TenantConnectionString.cs index 6fcfdc49e88..9bcd1abe461 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/TenantConnectionString.cs +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/TenantConnectionString.cs @@ -1,5 +1,6 @@ using System; using JetBrains.Annotations; +using Volo.Abp.Auditing; using Volo.Abp.Domain.Entities; namespace Volo.Abp.TenantManagement; @@ -10,6 +11,7 @@ public class TenantConnectionString : Entity public virtual string Name { get; protected set; } + [DisableAuditing] public virtual string Value { get; protected set; } protected TenantConnectionString() diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.HttpApi.Client/ClientProxies/multi-tenancy-generate-proxy.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.HttpApi.Client/ClientProxies/multi-tenancy-generate-proxy.json index de1f6494f6c..1122511d381 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.HttpApi.Client/ClientProxies/multi-tenancy-generate-proxy.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.HttpApi.Client/ClientProxies/multi-tenancy-generate-proxy.json @@ -496,7 +496,7 @@ "isOptional": false, "defaultValue": null, "constraintTypes": null, - "bindingSourceId": "ModelBinding", + "bindingSourceId": "Body", "descriptorName": "" } ], diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.HttpApi/Volo/Abp/TenantManagement/TenantController.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.HttpApi/Volo/Abp/TenantManagement/TenantController.cs index b3130f9eb4d..4312066e021 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.HttpApi/Volo/Abp/TenantManagement/TenantController.cs +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.HttpApi/Volo/Abp/TenantManagement/TenantController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using Volo.Abp.Application.Dtos; using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.Auditing; namespace Volo.Abp.TenantManagement; @@ -62,7 +63,7 @@ public virtual Task GetDefaultConnectionStringAsync(Guid id) [HttpPut] [Route("{id}/default-connection-string")] - public virtual Task UpdateDefaultConnectionStringAsync(Guid id, string defaultConnectionString) + public virtual Task UpdateDefaultConnectionStringAsync(Guid id, [FromBody] [DisableAuditing] string defaultConnectionString) { return TenantAppService.UpdateDefaultConnectionStringAsync(id, defaultConnectionString); } diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/wwwroot/client-proxies/multi-tenancy-proxy.js b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/wwwroot/client-proxies/multi-tenancy-proxy.js index 1ef5cddae98..242fb054a52 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/wwwroot/client-proxies/multi-tenancy-proxy.js +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/wwwroot/client-proxies/multi-tenancy-proxy.js @@ -58,9 +58,10 @@ volo.abp.tenantManagement.tenant.updateDefaultConnectionString = function(id, defaultConnectionString, ajaxParams) { return abp.ajax($.extend(true, { - url: abp.appPath + 'api/multi-tenancy/tenants/' + id + '/default-connection-string' + abp.utils.buildQueryString([{ name: 'defaultConnectionString', value: defaultConnectionString }]) + '', + url: abp.appPath + 'api/multi-tenancy/tenants/' + id + '/default-connection-string', type: 'PUT', - dataType: null + dataType: null, + data: JSON.stringify(defaultConnectionString) }, ajaxParams)); }; diff --git a/npm/ng-packs/packages/schematics/src/commands/api/files-service/proxy/__namespace@dir__/__name@kebab__.service.ts.template b/npm/ng-packs/packages/schematics/src/commands/api/files-service/proxy/__namespace@dir__/__name@kebab__.service.ts.template index aaa4220d9b3..70e3138fdd5 100644 --- a/npm/ng-packs/packages/schematics/src/commands/api/files-service/proxy/__namespace@dir__/__name@kebab__.service.ts.template +++ b/npm/ng-packs/packages/schematics/src/commands/api/files-service/proxy/__namespace@dir__/__name@kebab__.service.ts.template @@ -13,6 +13,10 @@ export class <%= name %>Service { const responseType = isBlob ? 'Blob' : body.responseType; const httpResponseType = body.httpResponseType; const acceptHeader = body.acceptHeader; + const headerEntries = []; + if (acceptHeader) { headerEntries.push("Accept: '" + acceptHeader + "'"); } + if (body.contentTypeHeader) { headerEntries.push("'Content-Type': '" + body.contentTypeHeader + "'"); } + const headers = headerEntries.length ? '{ ' + headerEntries.join(', ') + ' }' : ''; const resourceParameters = signature.parameters.filter(p => p.name !== 'config'); const resourceType = resourceParameters.length ? `{ ${resourceParameters.map(p => `${p.name}${p.optional}: ${p.type}`).join('; ')} }` @@ -33,8 +37,8 @@ export class <%= name %>Service { method: '<%= body.method %>',<% if (httpResponseType && httpResponseType !== 'json') { %> responseType: '<%= httpResponseType %>',<% } %><% - if (acceptHeader) { %> - headers: { Accept: '<%= acceptHeader %>' },<% } %> + if (headers) { %> + headers: <%= headers %>,<% } %> url: <%= body.url %>,<% if (body.dictParamVar && !body.params.length) { %> params: <%= body.dictParamVar %>,<% } %><% @@ -53,8 +57,8 @@ export class <%= name %>Service { method: '<%= body.method %>',<% if (httpResponseType && httpResponseType !== 'json') { %> responseType: '<%= httpResponseType %>',<% } %><% - if (acceptHeader) { %> - headers: { Accept: '<%= acceptHeader %>' },<% } %> + if (headers) { %> + headers: <%= headers %>,<% } %> url: <%= body.url %>,<% if (body.dictParamVar && !body.params.length) { %> params: <%= body.dictParamVar %>,<% } %><% diff --git a/npm/ng-packs/packages/schematics/src/models/method.ts b/npm/ng-packs/packages/schematics/src/models/method.ts index c160359770b..aa55077a2c8 100644 --- a/npm/ng-packs/packages/schematics/src/models/method.ts +++ b/npm/ng-packs/packages/schematics/src/models/method.ts @@ -47,6 +47,7 @@ export class Body { responseType: string; httpResponseType?: 'json' | 'text' | 'blob' | 'arraybuffer'; acceptHeader?: string; + contentTypeHeader?: string; url: string; registerActionParameter = (param: ParameterInBody) => { @@ -68,7 +69,15 @@ export class Body { this.params.push(paramName === value ? value : `${getParamName(paramName)}: ${value}`); break; case eBindingSourceId.FormFile: + this.body = value; + break; case eBindingSourceId.Body: + /* Angular sends a plain string body as text/plain, but the endpoint expects a JSON string. */ + if (param.typeSimple === 'string') { + this.body = `JSON.stringify(${value})`; + this.contentTypeHeader = 'application/json'; + break; + } this.body = value; break; case eBindingSourceId.Path: diff --git a/npm/ng-packs/packages/schematics/src/tests/action-to-body-mapper.spec.ts b/npm/ng-packs/packages/schematics/src/tests/action-to-body-mapper.spec.ts index 9e35e9ef8c0..021046fb579 100644 --- a/npm/ng-packs/packages/schematics/src/tests/action-to-body-mapper.spec.ts +++ b/npm/ng-packs/packages/schematics/src/tests/action-to-body-mapper.spec.ts @@ -76,6 +76,50 @@ describe('createActionToBodyMapper — string return value', () => { }); }); +describe('createActionToBodyMapper — body parameters', () => { + const mapBody = createActionToBodyMapper(); + + test('string body parameter is JSON encoded and gets an application/json content type', () => { + const body = mapBody( + buildAction({ + httpMethod: 'POST', + parameters: [ + { + nameOnMethod: 'connectionString', + name: 'connectionString', + type: 'System.String', + typeSimple: 'string', + bindingSourceId: eBindingSourceId.Body, + }, + ], + } as Partial), + ); + + expect(body.body).toBe('JSON.stringify(connectionString)'); + expect(body.contentTypeHeader).toBe('application/json'); + }); + + test('object body parameter is passed as is', () => { + const body = mapBody( + buildAction({ + httpMethod: 'POST', + parameters: [ + { + nameOnMethod: 'input', + name: 'input', + type: 'My.Project.UserDto', + typeSimple: 'My.Project.UserDto', + bindingSourceId: eBindingSourceId.Body, + }, + ], + } as Partial), + ); + + expect(body.body).toBe('input'); + expect(body.contentTypeHeader).toBeUndefined(); + }); +}); + describe('createActionToBodyMapper — IRemoteStreamContent return value', () => { const mapBody = createActionToBodyMapper(); diff --git a/npm/ng-packs/packages/schematics/src/tests/proxy-service-template-render.spec.ts b/npm/ng-packs/packages/schematics/src/tests/proxy-service-template-render.spec.ts index 611c0ef5990..10cdbd5da86 100644 --- a/npm/ng-packs/packages/schematics/src/tests/proxy-service-template-render.spec.ts +++ b/npm/ng-packs/packages/schematics/src/tests/proxy-service-template-render.spec.ts @@ -70,6 +70,7 @@ interface MockBody { responseTypeWithNamespace: string; httpResponseType?: string; acceptHeader?: string; + contentTypeHeader?: string; body?: string; params: string[]; dictParamVar?: string; @@ -85,6 +86,7 @@ function makeBody(overrides: Partial): MockBody { responseTypeWithNamespace: 'any', httpResponseType: undefined, acceptHeader: undefined, + contentTypeHeader: undefined, body: undefined, params: [], dictParamVar: undefined, @@ -106,6 +108,33 @@ describe('proxy service template — rendered output', () => { expect(output).not.toContain('headers:'); }); + test('string body emits a content type header and a JSON encoded body', () => { + const output = render(buildContext({ + method: 'POST', + body: 'JSON.stringify(connectionString)', + contentTypeHeader: 'application/json', + responseType: 'boolean', + responseTypeWithNamespace: 'boolean', + })); + + expect(output).toContain("headers: { 'Content-Type': 'application/json' }"); + expect(output).toContain('body: JSON.stringify(connectionString)'); + }); + + test('accept and content type headers are emitted together', () => { + const output = render(buildContext({ + method: 'POST', + body: 'JSON.stringify(value)', + contentTypeHeader: 'application/json', + acceptHeader: 'text/plain', + httpResponseType: 'text', + responseType: 'string', + responseTypeWithNamespace: 'string', + })); + + expect(output).toContain("headers: { Accept: 'text/plain', 'Content-Type': 'application/json' }"); + }); + test('resource api mode emits requestResource helper for GET methods', () => { const ctx = buildContext({ responseType: 'MyDto', diff --git a/npm/ng-packs/packages/tenant-management/proxy/src/lib/proxy/generate-proxy.json b/npm/ng-packs/packages/tenant-management/proxy/src/lib/proxy/generate-proxy.json index bb4cf826756..79b44f205ac 100644 --- a/npm/ng-packs/packages/tenant-management/proxy/src/lib/proxy/generate-proxy.json +++ b/npm/ng-packs/packages/tenant-management/proxy/src/lib/proxy/generate-proxy.json @@ -493,7 +493,7 @@ "isOptional": false, "defaultValue": null, "constraintTypes": null, - "bindingSourceId": "ModelBinding", + "bindingSourceId": "Body", "descriptorName": "" } ], diff --git a/npm/ng-packs/packages/tenant-management/proxy/src/lib/proxy/tenant.service.ts b/npm/ng-packs/packages/tenant-management/proxy/src/lib/proxy/tenant.service.ts index 28174f8ad80..7995594b387 100644 --- a/npm/ng-packs/packages/tenant-management/proxy/src/lib/proxy/tenant.service.ts +++ b/npm/ng-packs/packages/tenant-management/proxy/src/lib/proxy/tenant.service.ts @@ -67,8 +67,9 @@ export class TenantService { updateDefaultConnectionString = (id: string, defaultConnectionString: string) => this.restService.request({ method: 'PUT', + headers: { 'Content-Type': 'application/json' }, url: `/api/multi-tenancy/tenants/${id}/default-connection-string`, - params: { defaultConnectionString }, + body: JSON.stringify(defaultConnectionString), }, { apiName: this.apiName }); }