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
2 changes: 1 addition & 1 deletion docs/en/modules/tenant-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public virtual AuditLogActionInfo CreateAuditLogAction(
? type.FullName!
: "",
MethodName = method.Name,
Parameters = SerializeConvertArguments(arguments),
Parameters = SerializeConvertArguments(method, arguments),
ExecutionTime = Clock.Now
};

Expand Down Expand Up @@ -220,6 +220,24 @@ protected virtual void ExecutePreContributors(AuditLogInfo auditLogInfo)
}
}

protected virtual string SerializeConvertArguments(MethodInfo method, IDictionary<string, object?> 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<string, object?> arguments)
{
try
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<MyAuditedObject>();

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
{
}
Expand All @@ -135,5 +151,10 @@ public virtual Task DoItAsync()
{
return Task.CompletedTask;
}

public virtual Task DoItWithSecretAsync(string name, [DisableAuditing] string connectionString)
{
return Task.CompletedTask;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
using System;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
using Volo.Abp.Auditing;

namespace Volo.Abp.TenantManagement;

public interface ITenantAppService : ICrudAppService<TenantDto, Guid, GetTenantsInput, TenantCreateDto, TenantUpdateDto>
{
Task<string> GetDefaultConnectionStringAsync(Guid id);

Task UpdateDefaultConnectionStringAsync(Guid id, string defaultConnectionString);
Task UpdateDefaultConnectionStringAsync(Guid id, [DisableAuditing] string defaultConnectionString);

Task DeleteDefaultConnectionStringAsync(Guid id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -133,7 +134,7 @@ public virtual async Task<string> 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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using JetBrains.Annotations;
using Volo.Abp.Auditing;
using Volo.Abp.Domain.Entities;

namespace Volo.Abp.TenantManagement;
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@
"isOptional": false,
"defaultValue": null,
"constraintTypes": null,
"bindingSourceId": "ModelBinding",
"bindingSourceId": "Body",
"descriptorName": ""
}
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -62,7 +63,7 @@ public virtual Task<string> 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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('; ')} }`
Expand All @@ -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 %>,<% } %><%
Expand All @@ -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 %>,<% } %><%
Expand Down
9 changes: 9 additions & 0 deletions npm/ng-packs/packages/schematics/src/models/method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class Body {
responseType: string;
httpResponseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
acceptHeader?: string;
contentTypeHeader?: string;
url: string;

registerActionParameter = (param: ParameterInBody) => {
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Action>),
);

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<Action>),
);

expect(body.body).toBe('input');
expect(body.contentTypeHeader).toBeUndefined();
});
});

describe('createActionToBodyMapper — IRemoteStreamContent return value', () => {
const mapBody = createActionToBodyMapper();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ interface MockBody {
responseTypeWithNamespace: string;
httpResponseType?: string;
acceptHeader?: string;
contentTypeHeader?: string;
body?: string;
params: string[];
dictParamVar?: string;
Expand All @@ -85,6 +86,7 @@ function makeBody(overrides: Partial<MockBody>): MockBody {
responseTypeWithNamespace: 'any',
httpResponseType: undefined,
acceptHeader: undefined,
contentTypeHeader: undefined,
body: undefined,
params: [],
dictParamVar: undefined,
Expand All @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@
"isOptional": false,
"defaultValue": null,
"constraintTypes": null,
"bindingSourceId": "ModelBinding",
"bindingSourceId": "Body",
"descriptorName": ""
}
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ export class TenantService {
updateDefaultConnectionString = (id: string, defaultConnectionString: string) =>
this.restService.request<any, void>({
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 });
}
Loading