Merge branch 'develop' into feature/create-room

This commit is contained in:
Alexey Safronov 2022-08-22 16:14:48 +03:00
commit 5ede1f3069
45 changed files with 1725 additions and 796 deletions

14
build/Jenkinsfile vendored
View File

@ -43,20 +43,20 @@ pipeline {
stages {
stage('Components') {
steps {
sh "yarn install --frozen-lockfile && yarn build && cd ${env.WORKSPACE}/packages/asc-web-components && yarn test:coverage --ci --reporters=default --reporters=jest-junit || true"
sh "yarn install --frozen-lockfile && yarn build && cd ${env.WORKSPACE}/packages/components && yarn test:coverage --ci --reporters=default --reporters=jest-junit || true"
}
post {
success {
junit 'packages/asc-web-components/junit.xml'
junit 'packages/components/junit.xml'
publishHTML target: [
allowMissing : false,
alwaysLinkToLastBuild: false,
keepAll : true,
reportDir : 'packages/asc-web-components/coverage/lcov-report',
reportDir : 'packages/components/coverage/lcov-report',
reportFiles : 'index.html',
reportName : 'Unix Test Report'
]
publishCoverage adapters: [coberturaAdapter('packages/asc-web-components/coverage/cobertura-coverage.xml')]
publishCoverage adapters: [coberturaAdapter('packages/components/coverage/cobertura-coverage.xml')]
}
}
}
@ -72,16 +72,16 @@ pipeline {
stages {
stage('Components') {
steps {
bat "yarn install --frozen-lockfile && yarn build && cd ${env.WORKSPACE}\\packages\\asc-web-components && yarn test:coverage --ci --reporters=default --reporters=jest-junit || true"
bat "yarn install --frozen-lockfile && yarn build && cd ${env.WORKSPACE}\\packages\\components && yarn test:coverage --ci --reporters=default --reporters=jest-junit || true"
}
post {
success {
junit 'packages\\asc-web-components\\junit.xml'
junit 'packages\\components\\junit.xml'
publishHTML target: [
allowMissing : false,
alwaysLinkToLastBuild: false,
keepAll : true,
reportDir : 'packages\\asc-web-components\\coverage\\lcov-report',
reportDir : 'packages\\components\\coverage\\lcov-report',
reportFiles : 'index.html',
reportName : 'Windows Test Report'
]

View File

@ -1,5 +1,6 @@
@echo "MIGRATIONS"
@echo off
PUSHD %~dp0..\common\Tools\Migration.Creator
dotnet run --project Migration.Creator.csproj
PUSHD %~dp0..\common\Tools\ASC.Migration.Creator
dotnet run --project ASC.Migration.Creator.csproj
pause

View File

@ -18,6 +18,7 @@
<PackageReference Include="AspNetCore.HealthChecks.NpgSql" Version="6.0.2" />
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="6.0.5" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.7" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.1.0" />

View File

@ -0,0 +1,103 @@
// (c) Copyright Ascensio System SIA 2010-2022
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using System.Text;
using System.Text.RegularExpressions;
using SecurityContext = ASC.Core.SecurityContext;
namespace ASC.Api.Core.Auth;
[Scope]
public class BasicAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly UserManager _userManager;
private readonly SecurityContext _securityContext;
private readonly PasswordHasher _passwordHasher;
public BasicAuthHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock
) : base(options, logger, encoder, clock)
{
}
public BasicAuthHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
UserManager userManager,
SecurityContext securityContext,
PasswordHasher passwordHasher) : this(options, logger, encoder, clock)
{
_userManager = userManager;
_securityContext = securityContext;
_passwordHasher = passwordHasher;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
Response.Headers.Add("WWW-Authenticate", "Basic");
if (!Request.Headers.ContainsKey("Authorization"))
{
return Task.FromResult(AuthenticateResult.Fail("Authorization header missing."));
}
// Get authorization key
var authorizationHeader = Request.Headers["Authorization"].ToString();
var authHeaderRegex = new Regex(@"Basic (.*)");
if (!authHeaderRegex.IsMatch(authorizationHeader))
{
return Task.FromResult(AuthenticateResult.Fail("Authorization code not formatted properly."));
}
var authBase64 = Encoding.UTF8.GetString(Convert.FromBase64String(authHeaderRegex.Replace(authorizationHeader, "$1")));
var authSplit = authBase64.Split(Convert.ToChar(":"), 2);
var authUsername = authSplit[0];
var authPassword = authSplit.Length > 1 ? authSplit[1] : throw new Exception("Unable to get password");
try
{
var userInfo = _userManager.GetUserByEmail(authUsername);
var passwordHash = _passwordHasher.GetClientPassword(authPassword);
_securityContext.AuthenticateMe(userInfo.Email, passwordHash);
}
catch (Exception)
{
return Task.FromResult(AuthenticateResult.Fail("The username or password is not correct."));
}
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(Context.User, Scheme.Name)));
}
}

View File

@ -31,9 +31,9 @@ namespace ASC.Api.Core.Auth;
[Scope]
public class CookieAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly AuthorizationHelper _authorizationHelper;
private readonly SecurityContext _securityContext;
private readonly CookiesManager _cookiesManager;
private readonly IHttpContextAccessor _httpContextAccessor;
public CookieAuthHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
@ -42,31 +42,59 @@ public class CookieAuthHandler : AuthenticationHandler<AuthenticationSchemeOptio
ISystemClock clock)
: base(options, logger, encoder, clock) { }
public CookieAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock,
AuthorizationHelper authorizationHelper,
public CookieAuthHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
SecurityContext securityContext,
CookiesManager cookiesManager)
CookiesManager cookiesManager,
IHttpContextAccessor httpContextAccessor)
: this(options, logger, encoder, clock)
{
_authorizationHelper = authorizationHelper;
_securityContext = securityContext;
_cookiesManager = cookiesManager;
_httpContextAccessor = httpContextAccessor;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var result = _authorizationHelper.ProcessBasicAuthorization(out _);
if (!result)
try
{
_securityContext.Logout();
_cookiesManager.ClearCookies(CookiesType.AuthKey);
_cookiesManager.ClearCookies(CookiesType.SocketIO);
var authorization = _httpContextAccessor.HttpContext.Request.Cookies["asc_auth_key"] ?? _httpContextAccessor.HttpContext.Request.Headers["Authorization"].ToString();
if (string.IsNullOrEmpty(authorization))
{
throw new AuthenticationException(nameof(HttpStatusCode.Unauthorized));
}
authorization = authorization.Trim();
if (0 <= authorization.IndexOf("Bearer", 0))
{
authorization = authorization.Substring("Bearer ".Length);
}
if (!_securityContext.AuthenticateMe(authorization))
{
throw new AuthenticationException(nameof(HttpStatusCode.Unauthorized));
}
}
catch (Exception)
{
return Task.FromResult(AuthenticateResult.Fail(new AuthenticationException(nameof(HttpStatusCode.Unauthorized))));
}
finally
{
if (!_securityContext.IsAuthenticated)
{
_securityContext.Logout();
_cookiesManager.ClearCookies(CookiesType.AuthKey);
_cookiesManager.ClearCookies(CookiesType.SocketIO);
}
}
return Task.FromResult(
result ?
AuthenticateResult.Success(new AuthenticationTicket(Context.User, new AuthenticationProperties(), Scheme.Name)) :
AuthenticateResult.Fail(new AuthenticationException(nameof(HttpStatusCode.Unauthorized))));
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(Context.User, Scheme.Name)));
}
}

View File

@ -24,6 +24,11 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using System.IdentityModel.Tokens.Jwt;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Net.Http.Headers;
using JsonConverter = System.Text.Json.Serialization.JsonConverter;
namespace ASC.Api.Core;
@ -102,13 +107,13 @@ public abstract class BaseStartup
services.AddSingleton(jsonOptions);
DIHelper.AddControllers();
DIHelper.TryAdd<DisposeMiddleware>();
DIHelper.TryAdd<CultureMiddleware>();
DIHelper.TryAdd<IpSecurityFilter>();
DIHelper.TryAdd<PaymentFilter>();
DIHelper.TryAdd<ProductSecurityFilter>();
DIHelper.TryAdd<TenantStatusFilter>();
DIHelper.TryAdd<ConfirmAuthHandler>();
DIHelper.TryAdd<BasicAuthHandler>();
DIHelper.TryAdd<CookieAuthHandler>();
DIHelper.TryAdd<WebhooksGlobalFilterAttribute>();
@ -147,16 +152,72 @@ public abstract class BaseStartup
config.OutputFormatters.Add(new XmlOutputFormatter());
});
var authBuilder = services.AddAuthentication("cookie")
.AddScheme<AuthenticationSchemeOptions, CookieAuthHandler>("cookie", a => { });
if (ConfirmAddScheme)
var authBuilder = services.AddAuthentication(options =>
{
authBuilder.AddScheme<AuthenticationSchemeOptions, ConfirmAuthHandler>("confirm", a => { });
}
options.DefaultScheme = "MultiAuthSchemes";
options.DefaultChallengeScheme = "MultiAuthSchemes";
}).AddScheme<AuthenticationSchemeOptions, CookieAuthHandler>(CookieAuthenticationDefaults.AuthenticationScheme, a => { })
.AddScheme<AuthenticationSchemeOptions, BasicAuthHandler>("Basic", a => { })
.AddScheme<AuthenticationSchemeOptions, ConfirmAuthHandler>("confirm", a => { })
.AddJwtBearer("Bearer", options =>
{
options.Authority = _configuration["core:oidc:authority"];
options.IncludeErrorDetails = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = ctx =>
{
using var scope = ctx.HttpContext.RequestServices.CreateScope();
var securityContext = scope.ServiceProvider.GetService<ASC.Core.SecurityContext>();
var claimUserId = ctx.Principal.FindFirstValue("userId");
if (String.IsNullOrEmpty(claimUserId))
{
throw new Exception("Claim 'UserId' is not present in claim list");
}
var userId = new Guid(claimUserId);
securityContext.AuthenticateMeWithoutCookie(userId, ctx.Principal.Claims.ToList());
return Task.CompletedTask;
}
};
})
.AddPolicyScheme("MultiAuthSchemes", JwtBearerDefaults.AuthenticationScheme, options =>
{
options.ForwardDefaultSelector = context =>
{
var authorizationHeader = context.Request.Headers[HeaderNames.Authorization].FirstOrDefault();
if (String.IsNullOrEmpty(authorizationHeader)) return CookieAuthenticationDefaults.AuthenticationScheme;
if (authorizationHeader.StartsWith("Basic ")) return "Basic";
if (authorizationHeader.StartsWith("Bearer "))
{
var token = authorizationHeader.Substring("Bearer ".Length).Trim();
var jwtHandler = new JwtSecurityTokenHandler();
return (jwtHandler.CanReadToken(token) && jwtHandler.ReadJwtToken(token).Issuer.Equals(_configuration["core:oidc:authority"]))
? JwtBearerDefaults.AuthenticationScheme : CookieAuthenticationDefaults.AuthenticationScheme;
}
return CookieAuthenticationDefaults.AuthenticationScheme;
};
});
services.AddAutoMapper(GetAutoMapperProfileAssemblies());
if (!_hostEnvironment.IsDevelopment())
{
services.AddStartupTask<WarmupServicesStartupTask>()
@ -189,8 +250,6 @@ public abstract class BaseStartup
app.UseCultureMiddleware();
app.UseDisposeMiddleware();
app.UseEndpoints(endpoints =>
{
endpoints.MapCustom();

View File

@ -24,8 +24,6 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
global using Microsoft.Extensions.DependencyInjection.Extensions;
global using System.Collections.Concurrent;
global using System.ComponentModel;
global using System.Globalization;
global using System.Linq.Expressions;
@ -36,6 +34,7 @@ global using System.Runtime.Serialization;
global using System.Security;
global using System.Security.Authentication;
global using System.Security.Claims;
global using System.Text;
global using System.Text.Encodings.Web;
global using System.Text.Json;
global using System.Text.Json.Serialization;
@ -56,8 +55,8 @@ global using ASC.AuditTrail.Types;
global using ASC.Common;
global using ASC.Common.Caching;
global using ASC.Common.DependencyInjection;
global using ASC.Common.Log;
global using ASC.Common.Logging;
global using ASC.Common.Mapping;
global using ASC.Common.Notify.Engine;
global using ASC.Common.Threading;
global using ASC.Common.Utils;
@ -119,11 +118,14 @@ global using Microsoft.AspNetCore.WebUtilities;
global using Microsoft.EntityFrameworkCore;
global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.DependencyInjection.Extensions;
global using Microsoft.Extensions.Diagnostics.HealthChecks;
global using Microsoft.Extensions.Hosting;
global using Microsoft.Extensions.Logging;
global using Microsoft.Extensions.Options;
global using Microsoft.Extensions.Primitives;
global using Microsoft.AspNetCore.Authentication.JwtBearer;
global using Microsoft.IdentityModel.Tokens;
global using Newtonsoft.Json;
global using Newtonsoft.Json.Serialization;

View File

@ -24,6 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using CallContext = ASC.Common.Notify.Engine.CallContext;
namespace ASC.Api.Core.Middleware;
[Scope]

View File

@ -24,8 +24,6 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using System.Text;
namespace ASC.Api.Core.Middleware;
[Scope]
@ -34,12 +32,14 @@ public class WebhooksGlobalFilterAttribute : ResultFilterAttribute, IDisposable
private readonly MemoryStream _stream;
private Stream _bodyStream;
private readonly IWebhookPublisher _webhookPublisher;
private readonly ILogger<WebhooksGlobalFilterAttribute> _logger;
private static readonly List<string> _methodList = new List<string> { "POST", "UPDATE", "DELETE" };
public WebhooksGlobalFilterAttribute(IWebhookPublisher webhookPublisher)
public WebhooksGlobalFilterAttribute(IWebhookPublisher webhookPublisher, ILogger<WebhooksGlobalFilterAttribute> logger)
{
_stream = new MemoryStream();
_webhookPublisher = webhookPublisher;
_logger = logger;
}
public override void OnResultExecuting(ResultExecutingContext context)
@ -68,11 +68,18 @@ public class WebhooksGlobalFilterAttribute : ResultFilterAttribute, IDisposable
await _stream.CopyToAsync(_bodyStream);
context.HttpContext.Response.Body = _bodyStream;
var (method, routePattern) = GetData(context.HttpContext);
try
{
var (method, routePattern) = GetData(context.HttpContext);
var resultContent = Encoding.UTF8.GetString(_stream.ToArray());
var eventName = $"method: {method}, route: {routePattern}";
_webhookPublisher.Publish(eventName, resultContent);
var resultContent = Encoding.UTF8.GetString(_stream.ToArray());
await _webhookPublisher.PublishAsync(method, routePattern, resultContent);
}
catch (Exception e)
{
_logger.ErrorWithException(e);
}
}
}

View File

@ -77,7 +77,6 @@ public class DistributedTask
Publication(this);
}
[Obsolete("GetProperty<T> is deprecated, please use indexer this[propName] instead.")]
public T GetProperty<T>(string propName)
{
if (!_props.TryGetValue(propName, out var propValue))
@ -88,8 +87,7 @@ public class DistributedTask
return JsonSerializer.Deserialize<T>(propValue);
}
[Obsolete("SetProperty is deprecated, please use indexer this[propName] = propValue instead.")]
public void SetProperty(string propName, object propValue)
public void SetProperty<T>(string propName, T propValue)
{
_props[propName] = JsonSerializer.Serialize(propValue);
}

View File

@ -1,93 +0,0 @@
// (c) Copyright Ascensio System SIA 2010-2022
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
namespace ASC.Common.Web;
public class DisposableHttpContext : IDisposable
{
private const string Key = "disposable.key";
public object this[string key]
{
get => Items.ContainsKey(key) ? Items[key] : null;
set
{
if (value == null)
{
throw new ArgumentNullException();
}
if (value is not IDisposable)
{
throw new ArgumentException("Only IDisposable may be added!");
}
Items[key] = (IDisposable)value;
}
}
private Dictionary<string, IDisposable> Items
{
get
{
var table = (Dictionary<string, IDisposable>)_context.Items[Key];
if (table == null)
{
table = new Dictionary<string, IDisposable>(1);
_context.Items.Add(Key, table);
}
return table;
}
}
private readonly HttpContext _context;
private bool _isDisposed;
public DisposableHttpContext(HttpContext ctx)
{
ArgumentNullException.ThrowIfNull(ctx);
_context = ctx;
}
public void Dispose()
{
if (!_isDisposed)
{
foreach (var item in Items.Values)
{
try
{
item.Dispose();
}
catch { }
}
_isDisposed = true;
}
}
}

View File

@ -97,9 +97,10 @@ public class SecurityContext
public bool AuthenticateMe(string cookie)
{
if (!string.IsNullOrEmpty(cookie))
{
if (string.IsNullOrEmpty(cookie)) return false;
if (!_cookieStorage.DecryptCookie(cookie, out var tenant, out var userid, out var indexTenant, out var expire, out var indexUser, out var loginEventId))
{
if (cookie.Equals("Bearer", StringComparison.InvariantCulture))
{
var ipFrom = string.Empty;
@ -115,54 +116,6 @@ public class SecurityContext
}
_logger.InformationEmptyBearer(ipFrom, address);
}
else if (_cookieStorage.DecryptCookie(cookie, out var tenant, out var userid, out var indexTenant, out var expire, out var indexUser, out var loginEventId))
{
if (tenant != _tenantManager.GetCurrentTenant().Id)
{
return false;
}
var settingsTenant = _tenantCookieSettingsHelper.GetForTenant(tenant);
if (indexTenant != settingsTenant.Index)
{
return false;
}
if (expire != DateTime.MaxValue && expire < DateTime.UtcNow)
{
return false;
}
try
{
var settingsUser = _tenantCookieSettingsHelper.GetForUser(userid);
if (indexUser != settingsUser.Index)
{
return false;
}
var settingLoginEvents = _dbLoginEventsManager.GetLoginEventIds(tenant, userid).Result; // remove Result
if (loginEventId != 0 && !settingLoginEvents.Contains(loginEventId))
{
return false;
}
AuthenticateMeWithoutCookie(new UserAccount(new UserInfo { Id = userid }, tenant, _userFormatter));
return true;
}
catch (InvalidCredentialException ice)
{
_logger.AuthenticateDebug(cookie, tenant, userid, ice);
}
catch (SecurityException se)
{
_logger.AuthenticateDebug(cookie, tenant, userid, se);
}
catch (Exception err)
{
_logger.AuthenticateError(cookie, tenant, userid, err);
}
}
else
{
var ipFrom = string.Empty;
@ -179,7 +132,57 @@ public class SecurityContext
_logger.WarningCanNotDecrypt(cookie, ipFrom, address);
}
return false;
}
if (tenant != _tenantManager.GetCurrentTenant().Id)
{
return false;
}
var settingsTenant = _tenantCookieSettingsHelper.GetForTenant(tenant);
if (indexTenant != settingsTenant.Index)
{
return false;
}
if (expire != DateTime.MaxValue && expire < DateTime.UtcNow)
{
return false;
}
try
{
var settingsUser = _tenantCookieSettingsHelper.GetForUser(userid);
if (indexUser != settingsUser.Index)
{
return false;
}
var settingLoginEvents = _dbLoginEventsManager.GetLoginEventIds(tenant, userid).Result; // remove Result
if (loginEventId != 0 && !settingLoginEvents.Contains(loginEventId))
{
return false;
}
AuthenticateMeWithoutCookie(new UserAccount(new UserInfo { Id = userid }, tenant, _userFormatter));
return true;
}
catch (InvalidCredentialException ice)
{
_logger.AuthenticateDebug(cookie, tenant, userid, ice);
}
catch (SecurityException se)
{
_logger.AuthenticateDebug(cookie, tenant, userid, se);
}
catch (Exception err)
{
_logger.AuthenticateError(cookie, tenant, userid, err);
}
return false;
}

View File

@ -24,6 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ASC.Core.Common.EF.Model;
public class ModelBuilderWrapper
@ -75,6 +77,11 @@ public class ModelBuilderWrapper
return this;
}
public EntityTypeBuilder<T> Entity<T>() where T : class
{
return ModelBuilder.Entity<T>();
}
public void AddDbFunction()
{
ModelBuilder

View File

@ -30,117 +30,186 @@ namespace ASC.Webhooks.Core;
public class DbWorker
{
private readonly IDbContextFactory<WebhooksDbContext> _dbContextFactory;
private readonly TenantManager _tenantManager;
public DbWorker(IDbContextFactory<WebhooksDbContext> dbContextFactory, TenantManager tenantManager)
private readonly TenantManager _tenantManager;
private readonly AuthContext _authContext;
private int Tenant
{
get
{
return _tenantManager.GetCurrentTenant().Id;
}
}
public DbWorker(IDbContextFactory<WebhooksDbContext> dbContextFactory, TenantManager tenantManager, AuthContext authContext)
{
_dbContextFactory = dbContextFactory;
_tenantManager = tenantManager;
}
public void AddWebhookConfig(WebhooksConfig webhooksConfig)
{
webhooksConfig.TenantId = _tenantManager.GetCurrentTenant().Id;
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
_tenantManager = tenantManager;
_authContext = authContext;
}
var addObj = webhooksDbContext.WebhooksConfigs.Where(it =>
it.SecretKey == webhooksConfig.SecretKey &&
it.TenantId == webhooksConfig.TenantId &&
it.Uri == webhooksConfig.Uri).FirstOrDefault();
if (addObj != null)
{
return;
}
webhooksDbContext.WebhooksConfigs.Add(webhooksConfig);
webhooksDbContext.SaveChanges();
}
public int ConfigsNumber()
public async Task<WebhooksConfig> AddWebhookConfig(string name, string uri, string secretKey)
{
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
return webhooksDbContext.WebhooksConfigs.Count();
var toAdd = new WebhooksConfig { TenantId = Tenant, Uri = uri, SecretKey = secretKey, Name = name };
toAdd = await webhooksDbContext.AddOrUpdateAsync(r => r.WebhooksConfigs, toAdd);
await webhooksDbContext.SaveChangesAsync();
return toAdd;
}
public List<WebhooksLog> GetTenantWebhooks()
public async IAsyncEnumerable<WebhooksConfig> GetTenantWebhooks()
{
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
var q = webhooksDbContext.WebhooksConfigs
.AsNoTracking()
.Where(it => it.TenantId == Tenant)
.AsAsyncEnumerable();
await foreach (var webhook in q)
{
yield return webhook;
}
}
public IAsyncEnumerable<WebhooksConfig> GetWebhookConfigs()
{
var webhooksDbContext = _dbContextFactory.CreateDbContext();
return webhooksDbContext.WebhooksConfigs
.Where(t => t.TenantId == Tenant)
.AsAsyncEnumerable();
}
public async Task<WebhooksConfig> UpdateWebhookConfig(int id, string name, string uri, string key, bool? enabled)
{
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
var updateObj = await webhooksDbContext.WebhooksConfigs
.Where(it => it.TenantId == Tenant && it.Id == id)
.FirstOrDefaultAsync();
if (updateObj != null)
{
if (!string.IsNullOrEmpty(uri))
{
updateObj.Uri = uri;
}
if (!string.IsNullOrEmpty(name))
{
updateObj.Name = name;
}
if (!string.IsNullOrEmpty(key))
{
updateObj.SecretKey = key;
}
if (enabled.HasValue)
{
updateObj.Enabled = enabled.Value;
}
webhooksDbContext.WebhooksConfigs.Update(updateObj);
await webhooksDbContext.SaveChangesAsync();
}
return updateObj;
}
public async Task<WebhooksConfig> RemoveWebhookConfig(int id)
{
var tenant = _tenantManager.GetCurrentTenant().Id;
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
return webhooksDbContext.WebhooksLogs.Where(it => it.TenantId == tenant)
.Select(t => new WebhooksLog
{
Uid = t.Uid,
CreationTime = t.CreationTime,
RequestPayload = t.RequestPayload,
RequestHeaders = t.RequestHeaders,
ResponsePayload = t.ResponsePayload,
ResponseHeaders = t.ResponseHeaders,
Status = t.Status
}).ToList();
}
public List<WebhooksConfig> GetWebhookConfigs(int tenant)
{
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
return webhooksDbContext.WebhooksConfigs.Where(t => t.TenantId == tenant).ToList();
}
public WebhookEntry ReadFromJournal(int id)
{
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
return webhooksDbContext.WebhooksLogs
.Where(it => it.Id == id)
.Join(webhooksDbContext.WebhooksConfigs, t => t.ConfigId, t => t.ConfigId, (payload, config) => new { payload, config })
.Select(t => new WebhookEntry { Id = t.payload.Id, Payload = t.payload.RequestPayload, SecretKey = t.config.SecretKey, Uri = t.config.Uri })
.OrderBy(t => t.Id).FirstOrDefault();
}
public void RemoveWebhookConfig(WebhooksConfig webhooksConfig)
{
webhooksConfig.TenantId = _tenantManager.GetCurrentTenant().Id;
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
var removeObj = webhooksDbContext.WebhooksConfigs.Where(it =>
it.SecretKey == webhooksConfig.SecretKey &&
it.TenantId == webhooksConfig.TenantId &&
it.Uri == webhooksConfig.Uri).FirstOrDefault();
var removeObj = await webhooksDbContext.WebhooksConfigs
.Where(it => it.TenantId == tenant && it.Id == id)
.FirstOrDefaultAsync();
webhooksDbContext.WebhooksConfigs.Remove(removeObj);
webhooksDbContext.SaveChanges();
}
public void UpdateWebhookConfig(WebhooksConfig webhooksConfig)
{
webhooksConfig.TenantId = _tenantManager.GetCurrentTenant().Id;
await webhooksDbContext.SaveChangesAsync();
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
var updateObj = webhooksDbContext.WebhooksConfigs.Where(it =>
it.SecretKey == webhooksConfig.SecretKey &&
it.TenantId == webhooksConfig.TenantId &&
it.Uri == webhooksConfig.Uri).FirstOrDefault();
webhooksDbContext.WebhooksConfigs.Update(updateObj);
webhooksDbContext.SaveChanges();
}
public void UpdateWebhookJournal(int id, ProcessStatus status, string responsePayload, string responseHeaders, string requestHeaders)
return removeObj;
}
public IAsyncEnumerable<WebhooksLog> ReadJournal(int startIndex, int limit, DateTime? delivery, string hookname, string route)
{
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
var webhook = webhooksDbContext.WebhooksLogs.Where(t => t.Id == id).FirstOrDefault();
var webhooksDbContext = _dbContextFactory.CreateDbContext();
var q = webhooksDbContext.WebhooksLogs
.AsNoTracking()
.Where(r => r.TenantId == Tenant);
if (delivery.HasValue)
{
var date = delivery.Value;
q = q.Where(r => r.Delivery == date);
}
if (!string.IsNullOrEmpty(hookname))
{
q = q.Where(r => r.Config.Name == hookname);
}
if (!string.IsNullOrEmpty(route))
{
q = q.Where(r => r.Route == route);
}
if (startIndex != 0)
{
q = q.Skip(startIndex);
}
if (limit != 0)
{
q = q.Take(limit);
}
return q.OrderByDescending(t => t.Id).AsAsyncEnumerable();
}
public async Task<WebhooksLog> ReadJournal(int id)
{
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
return await webhooksDbContext.WebhooksLogs
.AsNoTracking()
.Where(it => it.Id == id)
.FirstOrDefaultAsync();
}
public async Task<WebhooksLog> WriteToJournal(WebhooksLog webhook)
{
webhook.TenantId = _tenantManager.GetCurrentTenant().Id;
webhook.Uid = _authContext.CurrentAccount.ID;
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
var entity = await webhooksDbContext.WebhooksLogs.AddAsync(webhook);
await webhooksDbContext.SaveChangesAsync();
return entity.Entity;
}
public async Task<WebhooksLog> UpdateWebhookJournal(int id, int status, DateTime delivery, string requestHeaders, string responsePayload, string responseHeaders)
{
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
var webhook = await webhooksDbContext.WebhooksLogs.Where(t => t.Id == id).FirstOrDefaultAsync();
webhook.Status = status;
webhook.RequestHeaders = requestHeaders;
webhook.ResponsePayload = responsePayload;
webhook.ResponseHeaders = responseHeaders;
webhook.RequestHeaders = requestHeaders;
webhooksDbContext.WebhooksLogs.Update(webhook);
webhooksDbContext.SaveChanges();
}
webhook.ResponseHeaders = responseHeaders;
webhook.Delivery = delivery;
public int WriteToJournal(WebhooksLog webhook)
{
using var webhooksDbContext = _dbContextFactory.CreateDbContext();
var entity = webhooksDbContext.WebhooksLogs.Add(webhook);
webhooksDbContext.SaveChanges();
return entity.Entity.Id;
webhooksDbContext.WebhooksLogs.Update(webhook);
await webhooksDbContext.SaveChangesAsync();
return webhook;
}
}

View File

@ -28,8 +28,8 @@ namespace ASC.Webhooks.Core.EF.Context;
public class WebhooksDbContext : DbContext
{
public virtual DbSet<WebhooksConfig> WebhooksConfigs { get; set; }
public virtual DbSet<WebhooksLog> WebhooksLogs { get; set; }
public DbSet<WebhooksConfig> WebhooksConfigs { get; set; }
public DbSet<WebhooksLog> WebhooksLogs { get; set; }
public WebhooksDbContext(DbContextOptions<WebhooksDbContext> options) : base(options) { }

View File

@ -25,12 +25,20 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
namespace ASC.Webhooks.Core.EF.Model;
public partial class WebhooksConfig
public class WebhooksConfig : BaseEntity
{
public int ConfigId { get; set; }
public int Id { get; set; }
public string Name { get; set; }
public string SecretKey { get; set; }
public int TenantId { get; set; }
public string Uri { get; set; }
public string Uri { get; set; }
public bool Enabled { get; set; }
public override object[] GetKeys()
{
return new object[] { Id };
}
}
public static class WebhooksConfigExtension
@ -46,15 +54,18 @@ public static class WebhooksConfigExtension
{
modelBuilder.Entity<WebhooksConfig>(entity =>
{
entity.HasKey(e => new { e.ConfigId })
.HasName("PRIMARY");
entity.HasKey(e => new { e.Id })
.HasName("PRIMARY");
entity.HasIndex(e => e.TenantId)
.HasDatabaseName("tenant_id");
entity.ToTable("webhooks_config")
.HasCharSet("utf8");
entity.Property(e => e.ConfigId)
entity.Property(e => e.Id)
.HasColumnType("int")
.HasColumnName("config_id");
.HasColumnName("id");
entity.Property(e => e.TenantId)
.HasColumnName("tenant_id")
@ -68,7 +79,17 @@ public static class WebhooksConfigExtension
entity.Property(e => e.SecretKey)
.HasMaxLength(50)
.HasColumnName("secret_key")
.HasDefaultValueSql("''");
.HasDefaultValueSql("''");
entity.Property(e => e.Name)
.HasMaxLength(50)
.HasColumnName("name")
.IsRequired();
entity.Property(e => e.Enabled)
.HasColumnName("enabled")
.HasDefaultValueSql("'1'")
.HasColumnType("tinyint(1)");
});
}
@ -76,14 +97,17 @@ public static class WebhooksConfigExtension
{
modelBuilder.Entity<WebhooksConfig>(entity =>
{
entity.HasKey(e => new { e.ConfigId })
entity.HasKey(e => new { e.Id })
.HasName("PRIMARY");
entity.ToTable("webhooks_config");
entity.ToTable("webhooks_config");
entity.HasIndex(e => e.TenantId)
.HasDatabaseName("tenant_id");
entity.Property(e => e.ConfigId)
entity.Property(e => e.Id)
.HasColumnType("int")
.HasColumnName("config_id");
.HasColumnName("id");
entity.Property(e => e.TenantId)
.HasColumnName("tenant_id")
@ -97,7 +121,16 @@ public static class WebhooksConfigExtension
entity.Property(e => e.SecretKey)
.HasMaxLength(50)
.HasColumnName("secret_key")
.HasDefaultValueSql("''");
.HasDefaultValueSql("''");
entity.Property(e => e.Name)
.HasMaxLength(50)
.HasColumnName("name")
.IsRequired();
entity.Property(e => e.Enabled)
.HasColumnName("enabled")
.HasDefaultValueSql("true");
});
}
}

View File

@ -26,25 +26,31 @@
namespace ASC.Webhooks.Core.EF.Model;
public partial class WebhooksLog
public class WebhooksLog
{
public int ConfigId { get; set; }
public DateTime CreationTime { get; set; }
public string Event { get; set; }
public int Id { get; set; }
public int Id { get; set; }
public string Method { get; set; }
public string Route { get; set; }
public string RequestHeaders { get; set; }
public string RequestPayload { get; set; }
public string ResponseHeaders { get; set; }
public string ResponsePayload { get; set; }
public ProcessStatus Status { get; set; }
public int Status { get; set; }
public int TenantId { get; set; }
public string Uid { get; set; }
public Guid Uid { get; set; }
public DateTime? Delivery { get; set; }
public WebhooksConfig Config { get; set; }
}
public static class WebhooksPayloadExtension
{
public static ModelBuilderWrapper AddWebhooksLog(this ModelBuilderWrapper modelBuilder)
{
{
modelBuilder.Entity<WebhooksLog>().Navigation(e => e.Config).AutoInclude();
modelBuilder
.Add(MySqlAddWebhooksLog, Provider.MySql)
.Add(PgSqlAddWebhooksLog, Provider.PostgreSql);
@ -60,7 +66,10 @@ public static class WebhooksPayloadExtension
.HasName("PRIMARY");
entity.ToTable("webhooks_logs")
.HasCharSet("utf8");
.HasCharSet("utf8");
entity.HasIndex(e => e.TenantId)
.HasDatabaseName("tenant_id");
entity.Property(e => e.Id)
.HasColumnType("int")
@ -72,9 +81,10 @@ public static class WebhooksPayloadExtension
.HasColumnName("config_id");
entity.Property(e => e.Uid)
.HasColumnType("varchar")
.HasColumnName("uid")
.HasMaxLength(50);
.HasColumnName("uid")
.HasColumnType("varchar(36)")
.HasCharSet("utf8")
.UseCollation("utf8_general_ci");
entity.Property(e => e.TenantId)
.HasColumnName("tenant_id")
@ -83,7 +93,9 @@ public static class WebhooksPayloadExtension
entity.Property(e => e.RequestPayload)
.IsRequired()
.HasColumnName("request_payload")
.HasColumnType("json");
.HasColumnType("text")
.HasCharSet("utf8")
.UseCollation("utf8_general_ci");
entity.Property(e => e.RequestHeaders)
.HasColumnName("request_headers")
@ -91,25 +103,35 @@ public static class WebhooksPayloadExtension
entity.Property(e => e.ResponsePayload)
.HasColumnName("response_payload")
.HasColumnType("json");
.HasColumnType("text")
.HasCharSet("utf8")
.UseCollation("utf8_general_ci");
entity.Property(e => e.ResponseHeaders)
.HasColumnName("response_headers")
.HasColumnType("json");
entity.Property(e => e.Event)
entity.Property(e => e.Method)
.HasColumnType("varchar")
.HasColumnName("event")
.HasColumnName("method")
.HasMaxLength(100);
entity.Property(e => e.Route)
.HasColumnType("varchar")
.HasColumnName("route")
.HasMaxLength(100);
entity.Property(e => e.CreationTime)
.HasColumnType("datetime")
.HasColumnName("creation_time");
entity.Property(e => e.Delivery)
.HasColumnType("datetime")
.HasColumnName("delivery");
entity.Property(e => e.Status)
.HasColumnType("varchar")
.HasColumnName("status")
.HasMaxLength(50);
.HasColumnType("int")
.HasColumnName("status");
});
}
@ -120,7 +142,10 @@ public static class WebhooksPayloadExtension
entity.HasKey(e => new { e.Id })
.HasName("PRIMARY");
entity.ToTable("webhooks_logs");
entity.ToTable("webhooks_logs");
entity.HasIndex(e => e.TenantId)
.HasDatabaseName("tenant_id");
entity.Property(e => e.Id)
.HasColumnType("int")
@ -142,34 +167,40 @@ public static class WebhooksPayloadExtension
entity.Property(e => e.RequestPayload)
.IsRequired()
.HasColumnName("request_payload")
.HasColumnType("json");
.HasColumnName("request_payload");
entity.Property(e => e.RequestHeaders)
.HasColumnName("request_headers")
.HasColumnType("json");
entity.Property(e => e.ResponsePayload)
.HasColumnName("response_payload")
.HasColumnType("json");
.HasColumnName("response_payload");
entity.Property(e => e.ResponseHeaders)
.HasColumnName("response_headers")
.HasColumnType("json");
entity.Property(e => e.Event)
entity.Property(e => e.Method)
.HasColumnType("varchar")
.HasColumnName("event")
.HasColumnName("method")
.HasMaxLength(100);
entity.Property(e => e.Route)
.HasColumnType("varchar")
.HasColumnName("route")
.HasMaxLength(100);
entity.Property(e => e.CreationTime)
.HasColumnType("datetime")
.HasColumnName("creation_time");
.HasColumnName("creation_time");
entity.Property(e => e.Delivery)
.HasColumnType("datetime")
.HasColumnName("delivery");
entity.Property(e => e.Status)
.HasColumnType("varchar")
.HasColumnName("status")
.HasMaxLength(50);
.HasColumnType("int")
.HasColumnName("status");
});
}
}

View File

@ -29,5 +29,6 @@ namespace ASC.Webhooks.Core;
[Scope]
public interface IWebhookPublisher
{
public void Publish(string eventName, string requestPayload);
public Task PublishAsync(string method, string route, string requestPayload);
public Task<WebhooksLog> PublishAsync(string method, string route, string requestPayload, int configId);
}

View File

@ -30,51 +30,56 @@ namespace ASC.Webhooks.Core;
public class WebhookPublisher : IWebhookPublisher
{
private readonly DbWorker _dbWorker;
private readonly TenantManager _tenantManager;
private readonly ICacheNotify<WebhookRequest> _webhookNotify;
public WebhookPublisher(
DbWorker dbWorker,
TenantManager tenantManager,
ICacheNotify<WebhookRequest> webhookNotify)
{
_dbWorker = dbWorker;
_tenantManager = tenantManager;
_webhookNotify = webhookNotify;
}
public void Publish(string eventName, string requestPayload)
public async Task PublishAsync(string method, string route, string requestPayload)
{
var tenantId = _tenantManager.GetCurrentTenant().Id;
var webhookConfigs = _dbWorker.GetWebhookConfigs(tenantId);
foreach (var config in webhookConfigs)
if (string.IsNullOrEmpty(requestPayload))
{
var webhooksLog = new WebhooksLog
{
Uid = Guid.NewGuid().ToString(),
TenantId = tenantId,
Event = eventName,
CreationTime = DateTime.UtcNow,
RequestPayload = requestPayload,
Status = ProcessStatus.InProcess,
ConfigId = config.ConfigId
};
var DbId = _dbWorker.WriteToJournal(webhooksLog);
return;
}
var webhookConfigs = _dbWorker.GetWebhookConfigs();
var request = new WebhookRequest()
{
Id = DbId
};
_webhookNotify.Publish(request, CacheNotifyAction.Update);
await foreach (var config in webhookConfigs.Where(r => r.Enabled))
{
_ = await PublishAsync(method, route, requestPayload, config.Id);
}
}
}
public enum ProcessStatus
{
InProcess,
Success,
Failed
public async Task<WebhooksLog> PublishAsync(string method, string route, string requestPayload, int configId)
{
if (string.IsNullOrEmpty(requestPayload))
{
return null;
}
var webhooksLog = new WebhooksLog
{
Method = method,
Route = route,
CreationTime = DateTime.UtcNow,
RequestPayload = requestPayload,
ConfigId = configId
};
var webhook = await _dbWorker.WriteToJournal(webhooksLog);
var request = new WebhookRequest
{
Id = webhook.Id
};
_webhookNotify.Publish(request, CacheNotifyAction.Update);
return webhook;
}
}

View File

@ -6,6 +6,10 @@
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="6.0.8" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ASC.Api.Core\ASC.Api.Core.csproj" />
</ItemGroup>

View File

@ -37,9 +37,13 @@ global using ASC.Common.Log;
global using ASC.Common.Utils;
global using ASC.Web.Webhooks;
global using ASC.Webhooks.Core;
global using ASC.Webhooks.Service;
global using ASC.Webhooks.Service.Log;
global using ASC.Webhooks.Service.Services;
global using Microsoft.AspNetCore.Builder;
global using Microsoft.Extensions.Hosting.WindowsServices;
global using Microsoft.Extensions.Logging;
global using Microsoft.Extensions.Logging;
global using Polly;
global using Polly.Extensions.Http;

View File

@ -41,11 +41,20 @@ builder.Host.ConfigureDefault(args, (hostContext, config, env, path) =>
diHelper.TryAdd<DbWorker>();
services.AddHostedService<BuildQueueService>();
diHelper.TryAdd<BuildQueueService>();
services.AddHostedService<WorkerService>();
diHelper.TryAdd<WorkerService>();
diHelper.TryAdd<WorkerService>();
services.AddHttpClient("webhook")
.SetHandlerLifetime(TimeSpan.FromMinutes(5))
.AddPolicyHandler((s, request) =>
{
var settings = s.GetRequiredService<Settings>();
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
.WaitAndRetryAsync(settings.RepeatCount.HasValue ? settings.RepeatCount.Value : 5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
});
});
builder.WebHost.ConfigureDefaultKestrel();

View File

@ -29,26 +29,36 @@ namespace ASC.Webhooks.Service.Services;
[Singletone]
public class WorkerService : BackgroundService
{
private readonly ILogger<WorkerService> _logger;
private readonly ConcurrentQueue<WebhookRequest> _queue;
private readonly ILogger<WorkerService> _logger;
private readonly int? _threadCount = 10;
private readonly WebhookSender _webhookSender;
private readonly TimeSpan _waitingPeriod;
private readonly TimeSpan _waitingPeriod;
private readonly ConcurrentQueue<WebhookRequest> _queue;
private readonly ICacheNotify<WebhookRequest> _webhookNotify;
public WorkerService(WebhookSender webhookSender,
public WorkerService(
ICacheNotify<WebhookRequest> webhookNotify,
WebhookSender webhookSender,
ILogger<WorkerService> logger,
BuildQueueService buildQueueService,
Settings settings)
{
_logger = logger;
{
_webhookNotify = webhookNotify;
_queue = new ConcurrentQueue<WebhookRequest>();
_logger = logger;
_webhookSender = webhookSender;
_queue = buildQueueService.Queue;
_threadCount = settings.ThreadCount;
_waitingPeriod = TimeSpan.FromSeconds(5);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
{
_webhookNotify.Subscribe(_queue.Enqueue, CacheNotifyAction.Update);
stoppingToken.Register(() =>
{
_webhookNotify.Unsubscribe(CacheNotifyAction.Update);
});
while (!stoppingToken.IsCancellationRequested)
{
var queueSize = _queue.Count;
@ -62,7 +72,7 @@ public class WorkerService : BackgroundService
continue;
}
var tasks = new List<Task>();
var tasks = new List<Task>(queueSize);
var counter = 0;
for (var i = 0; i < queueSize; i++)
@ -82,13 +92,13 @@ public class WorkerService : BackgroundService
if (counter >= _threadCount)
{
Task.WaitAll(tasks.ToArray());
await Task.WhenAll(tasks);
tasks.Clear();
counter = 0;
}
}
Task.WaitAll(tasks.ToArray());
}
await Task.WhenAll(tasks);
_logger.DebugProcedureFinish();
}
}

View File

@ -37,7 +37,7 @@ public class Settings
{
var cfg = configuration.GetSetting<Settings>("webhooks");
RepeatCount = cfg.RepeatCount ?? 5;
ThreadCount = cfg.ThreadCount ?? 1;
ThreadCount = cfg.ThreadCount ?? 10;
}
public int? RepeatCount { get; }
public int? ThreadCount { get; }

View File

@ -24,6 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using System.Text.Json.Serialization;
namespace ASC.Webhooks.Service;
[Singletone]
@ -31,15 +33,19 @@ public class WebhookSender
{
private readonly IHttpClientFactory _clientFactory;
private readonly ILogger _log;
public int? RepeatCount { get; init; }
private readonly IServiceScopeFactory _scopeFactory;
private readonly IServiceScopeFactory _scopeFactory;
private readonly JsonSerializerOptions _jsonSerializerOptions;
public WebhookSender(ILoggerProvider options, IServiceScopeFactory scopeFactory, Settings settings, IHttpClientFactory clientFactory)
public WebhookSender(ILoggerProvider options, IServiceScopeFactory scopeFactory, IHttpClientFactory clientFactory)
{
_log = options.CreateLogger("ASC.Webhooks.Core");
_scopeFactory = scopeFactory;
RepeatCount = settings.RepeatCount;
_clientFactory = clientFactory;
_clientFactory = clientFactory;
_jsonSerializerOptions = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
IgnoreReadOnlyProperties = true
};
}
public async Task Send(WebhookRequest webhookRequest, CancellationToken cancellationToken)
@ -47,82 +53,65 @@ public class WebhookSender
using var scope = _scopeFactory.CreateScope();
var dbWorker = scope.ServiceProvider.GetRequiredService<DbWorker>();
var entry = dbWorker.ReadFromJournal(webhookRequest.Id);
var id = entry.Id;
var requestURI = entry.Uri;
var secretKey = entry.SecretKey;
var data = entry.Payload;
var response = new HttpResponseMessage();
var request = new HttpRequestMessage();
for (var i = 0; i < RepeatCount; i++)
{
try
{
request = new HttpRequestMessage(HttpMethod.Post, requestURI);
request.Headers.Add("Accept", "*/*");
request.Headers.Add("Secret", "SHA256=" + GetSecretHash(secretKey, data));
request.Content = new StringContent(
data,
Encoding.UTF8,
"application/json");
var httpClient = _clientFactory.CreateClient();
response = await httpClient.SendAsync(request, cancellationToken);
if (response.IsSuccessStatusCode)
{
UpdateDb(dbWorker, id, response, request, ProcessStatus.Success);
_log.DebugResponse(response);
break;
}
else if (i == RepeatCount - 1)
{
UpdateDb(dbWorker, id, response, request, ProcessStatus.Failed);
_log.DebugResponse(response);
}
}
catch (Exception ex)
{
if (i == RepeatCount - 1)
{
UpdateDb(dbWorker, id, response, request, ProcessStatus.Failed);
}
_log.ErrorWithException(ex);
continue;
}
var entry = await dbWorker.ReadJournal(webhookRequest.Id);
var status = 0;
string responsePayload = null;
string responseHeaders = null;
string requestHeaders = null;
var delivery = DateTime.MinValue;
try
{
var httpClient = _clientFactory.CreateClient("webhook");
var request = new HttpRequestMessage(HttpMethod.Post, entry.Config.Uri)
{
Content = new StringContent(entry.RequestPayload, Encoding.UTF8, "application/json")
};
request.Headers.Add("Accept", "*/*");
request.Headers.Add("Secret", "SHA256=" + GetSecretHash(entry.Config.SecretKey, entry.RequestPayload));
requestHeaders = JsonSerializer.Serialize(request.Headers.ToDictionary(r => r.Key, v => v.Value), _jsonSerializerOptions);
var response = await httpClient.SendAsync(request, cancellationToken);
status = (int)response.StatusCode;
responseHeaders = JsonSerializer.Serialize(response.Headers.ToDictionary(r => r.Key, v => v.Value), _jsonSerializerOptions);
responsePayload = await response.Content.ReadAsStringAsync();
delivery = DateTime.UtcNow;
_log.DebugResponse(response);
}
catch (HttpRequestException e)
{
if (e.StatusCode.HasValue)
{
status = (int)e.StatusCode.Value;
}
responsePayload = e.Message;
delivery = DateTime.UtcNow;
_log.ErrorWithException(e);
}
catch (Exception e)
{
_log.ErrorWithException(e);
}
if (delivery != DateTime.MinValue)
{
await dbWorker.UpdateWebhookJournal(entry.Id, status, delivery, requestHeaders, responsePayload, responseHeaders);
}
}
private string GetSecretHash(string secretKey, string body)
{
string computedSignature;
{
var secretBytes = Encoding.UTF8.GetBytes(secretKey);
using (var hasher = new HMACSHA256(secretBytes))
{
var data = Encoding.UTF8.GetBytes(body);
computedSignature = BitConverter.ToString(hasher.ComputeHash(data));
return BitConverter.ToString(hasher.ComputeHash(data));
}
return computedSignature;
}
private void UpdateDb(DbWorker dbWorker, int id, HttpResponseMessage response, HttpRequestMessage request, ProcessStatus status)
{
var responseHeaders = JsonSerializer.Serialize(response.Headers.ToDictionary(r => r.Key, v => v.Value));
var requestHeaders = JsonSerializer.Serialize(request.Headers.ToDictionary(r => r.Key, v => v.Value));
string responsePayload;
using (var streamReader = new StreamReader(response.Content.ReadAsStream()))
{
var responseContent = streamReader.ReadToEnd();
responsePayload = JsonSerializer.Serialize(responseContent);
}
dbWorker.UpdateWebhookJournal(id, status, responsePayload, responseHeaders, requestHeaders);
}
}

View File

@ -34,6 +34,9 @@
"hosting": {
"intervalCheckRegisterInstanceInSeconds": "1",
"timeUntilUnregisterInSeconds": "15"
},
"oidc": {
"authority" : ""
}
},
"license": {
@ -50,7 +53,7 @@
"enabled": "true"
},
"version": {
"number": "11.5.0",
"number": "1.0.0",
"release": {
"date": "",
"sign": ""

View File

@ -0,0 +1,163 @@
// <auto-generated />
using System;
using ASC.Webhooks.Core.EF.Context;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace ASC.Migrations.MySql.Migrations.WebhooksDb
{
[DbContext(typeof(WebhooksDbContext))]
[Migration("20220818144209_WebhooksDbContext_Upgrade1")]
partial class WebhooksDbContext_Upgrade1
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("ASC.Webhooks.Core.EF.Model.WebhooksConfig", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasColumnName("enabled")
.HasDefaultValueSql("'1'");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)")
.HasColumnName("name");
b.Property<string>("SecretKey")
.ValueGeneratedOnAdd()
.HasMaxLength(50)
.HasColumnType("varchar(50)")
.HasColumnName("secret_key")
.HasDefaultValueSql("''");
b.Property<uint>("TenantId")
.HasColumnType("int unsigned")
.HasColumnName("tenant_id");
b.Property<string>("Uri")
.ValueGeneratedOnAdd()
.HasMaxLength(50)
.HasColumnType("varchar(50)")
.HasColumnName("uri")
.HasDefaultValueSql("''");
b.HasKey("Id")
.HasName("PRIMARY");
b.HasIndex("TenantId")
.HasDatabaseName("tenant_id");
b.ToTable("webhooks_config", (string)null);
b.HasAnnotation("MySql:CharSet", "utf8");
});
modelBuilder.Entity("ASC.Webhooks.Core.EF.Model.WebhooksLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<int>("ConfigId")
.HasColumnType("int")
.HasColumnName("config_id");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime")
.HasColumnName("creation_time");
b.Property<DateTime?>("Delivery")
.HasColumnType("datetime")
.HasColumnName("delivery");
b.Property<string>("Method")
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("method");
b.Property<string>("RequestHeaders")
.HasColumnType("json")
.HasColumnName("request_headers");
b.Property<string>("RequestPayload")
.IsRequired()
.HasColumnType("text")
.HasColumnName("request_payload")
.UseCollation("utf8_general_ci")
.HasAnnotation("MySql:CharSet", "utf8");
b.Property<string>("ResponseHeaders")
.HasColumnType("json")
.HasColumnName("response_headers");
b.Property<string>("ResponsePayload")
.HasColumnType("text")
.HasColumnName("response_payload")
.UseCollation("utf8_general_ci")
.HasAnnotation("MySql:CharSet", "utf8");
b.Property<string>("Route")
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("route");
b.Property<int>("Status")
.HasColumnType("int")
.HasColumnName("status");
b.Property<uint>("TenantId")
.HasColumnType("int unsigned")
.HasColumnName("tenant_id");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("varchar(36)")
.HasColumnName("uid")
.UseCollation("utf8_general_ci")
.HasAnnotation("MySql:CharSet", "utf8");
b.HasKey("Id")
.HasName("PRIMARY");
b.HasIndex("ConfigId");
b.HasIndex("TenantId")
.HasDatabaseName("tenant_id");
b.ToTable("webhooks_logs", (string)null);
b.HasAnnotation("MySql:CharSet", "utf8");
});
modelBuilder.Entity("ASC.Webhooks.Core.EF.Model.WebhooksLog", b =>
{
b.HasOne("ASC.Webhooks.Core.EF.Model.WebhooksConfig", "Config")
.WithMany()
.HasForeignKey("ConfigId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Config");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,151 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ASC.Migrations.MySql.Migrations.WebhooksDb
{
public partial class WebhooksDbContext_Upgrade1 : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "config_id",
table: "webhooks_config",
newName: "id");
migrationBuilder.UpdateData(
table: "webhooks_logs",
keyColumn: "uid",
keyValue: null,
column: "uid",
value: "");
migrationBuilder.AlterColumn<string>(
name: "uid",
table: "webhooks_logs",
type: "varchar(36)",
nullable: false,
collation: "utf8_general_ci",
oldClrType: typeof(string),
oldType: "varchar(50)",
oldMaxLength: 50,
oldNullable: true)
.Annotation("MySql:CharSet", "utf8")
.OldAnnotation("MySql:CharSet", "utf8");
migrationBuilder.AlterColumn<int>(
name: "status",
table: "webhooks_logs",
type: "int",
nullable: false,
oldClrType: typeof(string),
oldType: "varchar(50)",
oldMaxLength: 50)
.OldAnnotation("MySql:CharSet", "utf8");
migrationBuilder.AddColumn<DateTime>(
name: "delivery",
table: "webhooks_logs",
type: "datetime",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "enabled",
table: "webhooks_config",
type: "tinyint(1)",
nullable: false,
defaultValueSql: "'1'");
migrationBuilder.AddColumn<string>(
name: "name",
table: "webhooks_config",
type: "varchar(50)",
maxLength: 50,
nullable: false,
defaultValue: "")
.Annotation("MySql:CharSet", "utf8");
migrationBuilder.CreateIndex(
name: "IX_webhooks_logs_config_id",
table: "webhooks_logs",
column: "config_id");
migrationBuilder.CreateIndex(
name: "tenant_id",
table: "webhooks_logs",
column: "tenant_id");
migrationBuilder.CreateIndex(
name: "tenant_id",
table: "webhooks_config",
column: "tenant_id");
migrationBuilder.AddForeignKey(
name: "FK_webhooks_logs_webhooks_config_config_id",
table: "webhooks_logs",
column: "config_id",
principalTable: "webhooks_config",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_webhooks_logs_webhooks_config_config_id",
table: "webhooks_logs");
migrationBuilder.DropIndex(
name: "IX_webhooks_logs_config_id",
table: "webhooks_logs");
migrationBuilder.DropIndex(
name: "tenant_id",
table: "webhooks_logs");
migrationBuilder.DropIndex(
name: "tenant_id",
table: "webhooks_config");
migrationBuilder.DropColumn(
name: "delivery",
table: "webhooks_logs");
migrationBuilder.DropColumn(
name: "enabled",
table: "webhooks_config");
migrationBuilder.DropColumn(
name: "name",
table: "webhooks_config");
migrationBuilder.RenameColumn(
name: "id",
table: "webhooks_config",
newName: "config_id");
migrationBuilder.AlterColumn<string>(
name: "uid",
table: "webhooks_logs",
type: "varchar(50)",
maxLength: 50,
nullable: true,
oldClrType: typeof(string),
oldType: "varchar(36)",
oldCollation: "utf8_general_ci")
.Annotation("MySql:CharSet", "utf8")
.OldAnnotation("MySql:CharSet", "utf8");
migrationBuilder.AlterColumn<string>(
name: "status",
table: "webhooks_logs",
type: "varchar(50)",
maxLength: 50,
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:CharSet", "utf8");
}
}
}

View File

@ -16,15 +16,27 @@ namespace ASC.Migrations.MySql.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.4")
.HasAnnotation("ProductVersion", "6.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("ASC.Webhooks.Core.EF.Model.WebhooksConfig", b =>
{
b.Property<int>("ConfigId")
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("config_id");
.HasColumnName("id");
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasColumnName("enabled")
.HasDefaultValueSql("'1'");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)")
.HasColumnName("name");
b.Property<string>("SecretKey")
.ValueGeneratedOnAdd()
@ -44,9 +56,12 @@ namespace ASC.Migrations.MySql.Migrations
.HasColumnName("uri")
.HasDefaultValueSql("''");
b.HasKey("ConfigId")
b.HasKey("Id")
.HasName("PRIMARY");
b.HasIndex("TenantId")
.HasDatabaseName("tenant_id");
b.ToTable("webhooks_config", (string)null);
b.HasAnnotation("MySql:CharSet", "utf8");
@ -67,10 +82,14 @@ namespace ASC.Migrations.MySql.Migrations
.HasColumnType("datetime")
.HasColumnName("creation_time");
b.Property<string>("Event")
b.Property<DateTime?>("Delivery")
.HasColumnType("datetime")
.HasColumnName("delivery");
b.Property<string>("Method")
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("event");
.HasColumnName("method");
b.Property<string>("RequestHeaders")
.HasColumnType("json")
@ -78,21 +97,28 @@ namespace ASC.Migrations.MySql.Migrations
b.Property<string>("RequestPayload")
.IsRequired()
.HasColumnType("json")
.HasColumnName("request_payload");
.HasColumnType("text")
.HasColumnName("request_payload")
.UseCollation("utf8_general_ci")
.HasAnnotation("MySql:CharSet", "utf8");
b.Property<string>("ResponseHeaders")
.HasColumnType("json")
.HasColumnName("response_headers");
b.Property<string>("ResponsePayload")
.HasColumnType("json")
.HasColumnName("response_payload");
.HasColumnType("text")
.HasColumnName("response_payload")
.UseCollation("utf8_general_ci")
.HasAnnotation("MySql:CharSet", "utf8");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)")
b.Property<string>("Route")
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("route");
b.Property<int>("Status")
.HasColumnType("int")
.HasColumnName("status");
b.Property<uint>("TenantId")
@ -100,17 +126,35 @@ namespace ASC.Migrations.MySql.Migrations
.HasColumnName("tenant_id");
b.Property<string>("Uid")
.HasMaxLength(50)
.HasColumnType("varchar(50)")
.HasColumnName("uid");
.IsRequired()
.HasColumnType("varchar(36)")
.HasColumnName("uid")
.UseCollation("utf8_general_ci")
.HasAnnotation("MySql:CharSet", "utf8");
b.HasKey("Id")
.HasName("PRIMARY");
b.HasIndex("ConfigId");
b.HasIndex("TenantId")
.HasDatabaseName("tenant_id");
b.ToTable("webhooks_logs", (string)null);
b.HasAnnotation("MySql:CharSet", "utf8");
});
modelBuilder.Entity("ASC.Webhooks.Core.EF.Model.WebhooksLog", b =>
{
b.HasOne("ASC.Webhooks.Core.EF.Model.WebhooksConfig", "Config")
.WithMany()
.HasForeignKey("ConfigId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Config");
});
#pragma warning restore 612, 618
}
}

View File

@ -0,0 +1,158 @@
// <auto-generated />
using System;
using ASC.Webhooks.Core.EF.Context;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace ASC.Migrations.PostgreSql.Migrations.WebhooksDb
{
[DbContext(typeof(WebhooksDbContext))]
[Migration("20220818144209_WebhooksDbContext_Upgrade1")]
partial class WebhooksDbContext_Upgrade1
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
.HasAnnotation("ProductVersion", "6.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
modelBuilder.Entity("ASC.Webhooks.Core.EF.Model.WebhooksConfig", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasColumnName("enabled")
.HasDefaultValueSql("true");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("name");
b.Property<string>("SecretKey")
.ValueGeneratedOnAdd()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("secret_key")
.HasDefaultValueSql("''");
b.Property<int>("TenantId")
.HasColumnType("int unsigned")
.HasColumnName("tenant_id");
b.Property<string>("Uri")
.ValueGeneratedOnAdd()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("uri")
.HasDefaultValueSql("''");
b.HasKey("Id")
.HasName("PRIMARY");
b.HasIndex("TenantId")
.HasDatabaseName("tenant_id");
b.ToTable("webhooks_config", (string)null);
});
modelBuilder.Entity("ASC.Webhooks.Core.EF.Model.WebhooksLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("ConfigId")
.HasColumnType("int")
.HasColumnName("config_id");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime")
.HasColumnName("creation_time");
b.Property<DateTime?>("Delivery")
.HasColumnType("datetime")
.HasColumnName("delivery");
b.Property<string>("Method")
.HasMaxLength(100)
.HasColumnType("varchar")
.HasColumnName("method");
b.Property<string>("RequestHeaders")
.HasColumnType("json")
.HasColumnName("request_headers");
b.Property<string>("RequestPayload")
.IsRequired()
.HasColumnType("text")
.HasColumnName("request_payload");
b.Property<string>("ResponseHeaders")
.HasColumnType("json")
.HasColumnName("response_headers");
b.Property<string>("ResponsePayload")
.HasColumnType("text")
.HasColumnName("response_payload");
b.Property<string>("Route")
.HasMaxLength(100)
.HasColumnType("varchar")
.HasColumnName("route");
b.Property<int>("Status")
.HasColumnType("int")
.HasColumnName("status");
b.Property<int>("TenantId")
.HasColumnType("int unsigned")
.HasColumnName("tenant_id");
b.Property<string>("Uid")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar")
.HasColumnName("uid");
b.HasKey("Id")
.HasName("PRIMARY");
b.HasIndex("ConfigId");
b.HasIndex("TenantId")
.HasDatabaseName("tenant_id");
b.ToTable("webhooks_logs", (string)null);
});
modelBuilder.Entity("ASC.Webhooks.Core.EF.Model.WebhooksLog", b =>
{
b.HasOne("ASC.Webhooks.Core.EF.Model.WebhooksConfig", "Config")
.WithMany()
.HasForeignKey("ConfigId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Config");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,138 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ASC.Migrations.PostgreSql.Migrations.WebhooksDb
{
public partial class WebhooksDbContext_Upgrade1 : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "config_id",
table: "webhooks_config",
newName: "id");
migrationBuilder.AlterColumn<string>(
name: "uid",
table: "webhooks_logs",
type: "varchar",
maxLength: 50,
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "varchar",
oldMaxLength: 50,
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "status",
table: "webhooks_logs",
type: "int",
nullable: false,
oldClrType: typeof(string),
oldType: "varchar",
oldMaxLength: 50);
migrationBuilder.AddColumn<DateTime>(
name: "delivery",
table: "webhooks_logs",
type: "datetime",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "enabled",
table: "webhooks_config",
type: "boolean",
nullable: false,
defaultValueSql: "true");
migrationBuilder.AddColumn<string>(
name: "name",
table: "webhooks_config",
type: "character varying(50)",
maxLength: 50,
nullable: false,
defaultValue: "");
migrationBuilder.CreateIndex(
name: "IX_webhooks_logs_config_id",
table: "webhooks_logs",
column: "config_id");
migrationBuilder.CreateIndex(
name: "tenant_id",
table: "webhooks_logs",
column: "tenant_id");
migrationBuilder.CreateIndex(
name: "tenant_id",
table: "webhooks_config",
column: "tenant_id");
migrationBuilder.AddForeignKey(
name: "FK_webhooks_logs_webhooks_config_config_id",
table: "webhooks_logs",
column: "config_id",
principalTable: "webhooks_config",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_webhooks_logs_webhooks_config_config_id",
table: "webhooks_logs");
migrationBuilder.DropIndex(
name: "IX_webhooks_logs_config_id",
table: "webhooks_logs");
migrationBuilder.DropIndex(
name: "tenant_id",
table: "webhooks_logs");
migrationBuilder.DropIndex(
name: "tenant_id",
table: "webhooks_config");
migrationBuilder.DropColumn(
name: "delivery",
table: "webhooks_logs");
migrationBuilder.DropColumn(
name: "enabled",
table: "webhooks_config");
migrationBuilder.DropColumn(
name: "name",
table: "webhooks_config");
migrationBuilder.RenameColumn(
name: "id",
table: "webhooks_config",
newName: "config_id");
migrationBuilder.AlterColumn<string>(
name: "uid",
table: "webhooks_logs",
type: "varchar",
maxLength: 50,
nullable: true,
oldClrType: typeof(string),
oldType: "varchar",
oldMaxLength: 50);
migrationBuilder.AlterColumn<string>(
name: "status",
table: "webhooks_logs",
type: "varchar",
maxLength: 50,
nullable: false,
oldClrType: typeof(int),
oldType: "int");
}
}
}

View File

@ -18,17 +18,29 @@ namespace ASC.Migrations.PostgreSql.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
.HasAnnotation("ProductVersion", "6.0.4")
.HasAnnotation("ProductVersion", "6.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
modelBuilder.Entity("ASC.Webhooks.Core.EF.Model.WebhooksConfig", b =>
{
b.Property<int>("ConfigId")
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("config_id")
.HasColumnName("id")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasColumnName("enabled")
.HasDefaultValueSql("true");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("name");
b.Property<string>("SecretKey")
.ValueGeneratedOnAdd()
.HasMaxLength(50)
@ -47,9 +59,12 @@ namespace ASC.Migrations.PostgreSql.Migrations
.HasColumnName("uri")
.HasDefaultValueSql("''");
b.HasKey("ConfigId")
b.HasKey("Id")
.HasName("PRIMARY");
b.HasIndex("TenantId")
.HasDatabaseName("tenant_id");
b.ToTable("webhooks_config", (string)null);
});
@ -69,10 +84,14 @@ namespace ASC.Migrations.PostgreSql.Migrations
.HasColumnType("datetime")
.HasColumnName("creation_time");
b.Property<string>("Event")
b.Property<DateTime?>("Delivery")
.HasColumnType("datetime")
.HasColumnName("delivery");
b.Property<string>("Method")
.HasMaxLength(100)
.HasColumnType("varchar")
.HasColumnName("event");
.HasColumnName("method");
b.Property<string>("RequestHeaders")
.HasColumnType("json")
@ -80,7 +99,7 @@ namespace ASC.Migrations.PostgreSql.Migrations
b.Property<string>("RequestPayload")
.IsRequired()
.HasColumnType("json")
.HasColumnType("text")
.HasColumnName("request_payload");
b.Property<string>("ResponseHeaders")
@ -88,13 +107,16 @@ namespace ASC.Migrations.PostgreSql.Migrations
.HasColumnName("response_headers");
b.Property<string>("ResponsePayload")
.HasColumnType("json")
.HasColumnType("text")
.HasColumnName("response_payload");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(50)
b.Property<string>("Route")
.HasMaxLength(100)
.HasColumnType("varchar")
.HasColumnName("route");
b.Property<int>("Status")
.HasColumnType("int")
.HasColumnName("status");
b.Property<int>("TenantId")
@ -102,6 +124,7 @@ namespace ASC.Migrations.PostgreSql.Migrations
.HasColumnName("tenant_id");
b.Property<string>("Uid")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar")
.HasColumnName("uid");
@ -109,8 +132,24 @@ namespace ASC.Migrations.PostgreSql.Migrations
b.HasKey("Id")
.HasName("PRIMARY");
b.HasIndex("ConfigId");
b.HasIndex("TenantId")
.HasDatabaseName("tenant_id");
b.ToTable("webhooks_logs", (string)null);
});
modelBuilder.Entity("ASC.Webhooks.Core.EF.Model.WebhooksLog", b =>
{
b.HasOne("ASC.Webhooks.Core.EF.Model.WebhooksConfig", "Config")
.WithMany()
.HasForeignKey("ConfigId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Config");
});
#pragma warning restore 612, 618
}
}

View File

@ -238,8 +238,21 @@ public class FileStorageService<T> //: IFileStorageService
}));
}
public async Task<DataWrapper<T>> GetFolderItemsAsync(T parentId, int from, int count, FilterType filterType, bool subjectGroup, string subject, string searchText,
bool searchInContent, bool withSubfolders, OrderBy orderBy, SearchArea searchArea = SearchArea.Active, bool withoutTags = false, IEnumerable<string> tagNames = null, bool withoutMe = false)
public async Task<DataWrapper<T>> GetFolderItemsAsync(
T parentId,
int from,
int count,
FilterType filterType,
bool subjectGroup,
string subject,
string searchText,
bool searchInContent,
bool withSubfolders,
OrderBy orderBy,
SearchArea searchArea = SearchArea.Active,
bool withoutTags = false,
IEnumerable<string> tagNames = null,
bool withoutMe = false)
{
var subjectId = string.IsNullOrEmpty(subject) ? Guid.Empty : new Guid(subject);

View File

@ -121,8 +121,8 @@ public abstract class FoldersController<T> : ApiControllerBase
/// <param name="userIdOrGroupId" optional="true">User or group ID</param>
/// <param name="filterType" optional="true" remark="Allowed values: None (0), FilesOnly (1), FoldersOnly (2), DocumentsOnly (3), PresentationsOnly (4), SpreadsheetsOnly (5) or ImagesOnly (7)">Filter type</param>
/// <returns>Folder contents</returns>
[HttpGet("{folderId}")]
public async Task<FolderContentDto<T>> GetFolderAsync(T folderId, Guid userIdOrGroupId, FilterType filterType, bool searchInContent, bool withsubfolders)
[HttpGet("{folderId}", Order = 1)]
public async Task<FolderContentDto<T>> GetFolderAsync(T folderId, Guid? userIdOrGroupId, FilterType? filterType, bool? searchInContent, bool? withsubfolders)
{
var folder = await _foldersControllerHelper.GetFolderAsync(folderId, userIdOrGroupId, filterType, searchInContent, withsubfolders);
@ -223,7 +223,7 @@ public class FoldersControllerCommon : ApiControllerBase
/// <category>Folders</category>
/// <returns>Common folder contents</returns>
[HttpGet("@common")]
public async Task<FolderContentDto<int>> GetCommonFolderAsync(Guid userIdOrGroupId, FilterType filterType, bool searchInContent, bool withsubfolders)
public async Task<FolderContentDto<int>> GetCommonFolderAsync(Guid? userIdOrGroupId, FilterType? filterType, bool? searchInContent, bool? withsubfolders)
{
return await _foldersControllerHelperInt.GetFolderAsync(await _globalFolderHelper.FolderCommonAsync, userIdOrGroupId, filterType, searchInContent, withsubfolders);
}
@ -235,7 +235,7 @@ public class FoldersControllerCommon : ApiControllerBase
/// <category>Folders</category>
/// <returns>Favorites contents</returns>
[HttpGet("@favorites")]
public async Task<FolderContentDto<int>> GetFavoritesFolderAsync(Guid userIdOrGroupId, FilterType filterType, bool searchInContent, bool withsubfolders)
public async Task<FolderContentDto<int>> GetFavoritesFolderAsync(Guid? userIdOrGroupId, FilterType? filterType, bool? searchInContent, bool? withsubfolders)
{
return await _foldersControllerHelperInt.GetFolderAsync(await _globalFolderHelper.FolderFavoritesAsync, userIdOrGroupId, filterType, searchInContent, withsubfolders);
}
@ -249,13 +249,13 @@ public class FoldersControllerCommon : ApiControllerBase
/// <category>Folders</category>
/// <returns>My folder contents</returns>
[HttpGet("@my")]
public Task<FolderContentDto<int>> GetMyFolderAsync(Guid userIdOrGroupId, FilterType filterType, bool searchInContent, bool withsubfolders)
public Task<FolderContentDto<int>> GetMyFolderAsync(Guid? userIdOrGroupId, FilterType? filterType, bool? searchInContent, bool? withsubfolders)
{
return _foldersControllerHelperInt.GetFolderAsync(_globalFolderHelper.FolderMy, userIdOrGroupId, filterType, searchInContent, withsubfolders);
}
[HttpGet("@privacy")]
public async Task<FolderContentDto<int>> GetPrivacyFolderAsync(Guid userIdOrGroupId, FilterType filterType, bool searchInContent, bool withsubfolders)
public async Task<FolderContentDto<int>> GetPrivacyFolderAsync(Guid? userIdOrGroupId, FilterType? filterType, bool? searchInContent, bool? withsubfolders)
{
if (PrivacyRoomSettings.IsAvailable())
{
@ -274,7 +274,7 @@ public class FoldersControllerCommon : ApiControllerBase
/// <category>Folders</category>
/// <returns>Projects folder contents</returns>
[HttpGet("@projects")]
public async Task<FolderContentDto<string>> GetProjectsFolderAsync(Guid userIdOrGroupId, FilterType filterType, bool searchInContent, bool withsubfolders)
public async Task<FolderContentDto<string>> GetProjectsFolderAsync(Guid? userIdOrGroupId, FilterType? filterType, bool? searchInContent, bool? withsubfolders)
{
return await _foldersControllerHelperString.GetFolderAsync(await _globalFolderHelper.GetFolderProjectsAsync<string>(), userIdOrGroupId, filterType, searchInContent, withsubfolders);
}
@ -286,15 +286,15 @@ public class FoldersControllerCommon : ApiControllerBase
/// <category>Folders</category>
/// <returns>Recent contents</returns>
[HttpGet("@recent")]
public async Task<FolderContentDto<int>> GetRecentFolderAsync(Guid userIdOrGroupId, FilterType filterType, bool searchInContent, bool withsubfolders)
public async Task<FolderContentDto<int>> GetRecentFolderAsync(Guid? userIdOrGroupId, FilterType? filterType, bool? searchInContent, bool? withsubfolders)
{
return await _foldersControllerHelperInt.GetFolderAsync(await _globalFolderHelper.FolderRecentAsync, userIdOrGroupId, filterType, searchInContent, withsubfolders);
}
[HttpGet("@root")]
public async IAsyncEnumerable<FolderContentDto<int>> GetRootFoldersAsync(Guid userIdOrGroupId, FilterType filterType, bool withsubfolders, bool withoutTrash, bool searchInContent, bool withoutAdditionalFolder)
public async IAsyncEnumerable<FolderContentDto<int>> GetRootFoldersAsync(Guid? userIdOrGroupId, FilterType? filterType, bool? withsubfolders, bool? withoutTrash, bool? searchInContent, bool? withoutAdditionalFolder)
{
var foldersIds = _foldersControllerHelperInt.GetRootFoldersIdsAsync(withoutTrash, withoutAdditionalFolder);
var foldersIds = _foldersControllerHelperInt.GetRootFoldersIdsAsync(withoutTrash ?? false, withoutAdditionalFolder ?? false);
await foreach (var folder in foldersIds)
{
@ -311,7 +311,7 @@ public class FoldersControllerCommon : ApiControllerBase
/// <category>Folders</category>
/// <returns>Shared folder contents</returns>
[HttpGet("@share")]
public async Task<FolderContentDto<int>> GetShareFolderAsync(Guid userIdOrGroupId, FilterType filterType, bool searchInContent, bool withsubfolders)
public async Task<FolderContentDto<int>> GetShareFolderAsync(Guid? userIdOrGroupId, FilterType? filterType, bool? searchInContent, bool? withsubfolders)
{
return await _foldersControllerHelperInt.GetFolderAsync(await _globalFolderHelper.FolderShareAsync, userIdOrGroupId, filterType, searchInContent, withsubfolders);
}
@ -323,7 +323,7 @@ public class FoldersControllerCommon : ApiControllerBase
/// <category>Folders</category>
/// <returns>Templates contents</returns>
[HttpGet("@templates")]
public async Task<FolderContentDto<int>> GetTemplatesFolderAsync(Guid userIdOrGroupId, FilterType filterType, bool searchInContent, bool withsubfolders)
public async Task<FolderContentDto<int>> GetTemplatesFolderAsync(Guid? userIdOrGroupId, FilterType? filterType, bool? searchInContent, bool? withsubfolders)
{
return await _foldersControllerHelperInt.GetFolderAsync(await _globalFolderHelper.FolderTemplatesAsync, userIdOrGroupId, filterType, searchInContent, withsubfolders);
}
@ -337,7 +337,7 @@ public class FoldersControllerCommon : ApiControllerBase
/// <category>Folders</category>
/// <returns>Trash folder contents</returns>
[HttpGet("@trash")]
public Task<FolderContentDto<int>> GetTrashFolderAsync(Guid userIdOrGroupId, FilterType filterType, bool searchInContent, bool withsubfolders)
public Task<FolderContentDto<int>> GetTrashFolderAsync(Guid? userIdOrGroupId, FilterType? filterType, bool? searchInContent, bool? withsubfolders)
{
return _foldersControllerHelperInt.GetFolderAsync(Convert.ToInt32(_globalFolderHelper.FolderTrash), userIdOrGroupId, filterType, searchInContent, withsubfolders);
}

View File

@ -182,7 +182,7 @@ public abstract class VirtualRoomsController<T> : ApiControllerBase
/// Room content
/// </returns>
[HttpGet("rooms/{id}")]
public async Task<FolderContentDto<T>> GetRoomAsync(T id, Guid userOrGroupId, FilterType filterType, bool searchInContent, bool withSubFolders)
public async Task<FolderContentDto<T>> GetRoomAsync(T id, Guid? userOrGroupId, FilterType? filterType, bool? searchInContent, bool? withSubFolders)
{
ErrorIfNotDocSpace();
@ -669,8 +669,7 @@ public class VirtualRoomsCommonController : ApiControllerBase
/// Virtual Rooms content
/// </returns>
[HttpGet("rooms")]
public async Task<FolderContentDto<int>> GetRoomsFolderAsync(RoomType type, string subjectId, bool searchInContent, bool withSubfolders, SearchArea searchArea, bool withoutTags, string tags,
bool withoutMe)
public async Task<FolderContentDto<int>> GetRoomsFolderAsync(RoomType? type, string subjectId, bool? searchInContent, bool? withSubfolders, SearchArea? searchArea, bool? withoutTags, string tags, bool? withoutMe)
{
ErrorIfNotDocSpace();
@ -700,7 +699,7 @@ public class VirtualRoomsCommonController : ApiControllerBase
var filterValue = _apiContext.FilterValue;
var content = await _fileStorageService.GetFolderItemsAsync(parentId, startIndex, count, filterType, false, subjectId, filterValue,
searchInContent, withSubfolders, orderBy, searchArea, withoutTags, tagNames, withoutMe);
searchInContent ?? false, withSubfolders ?? false, orderBy, searchArea ?? SearchArea.Active, withoutTags ?? false, tagNames, withoutMe ?? false);
var dto = await _folderContentDtoHelper.GetAsync(content, startIndex);

View File

@ -77,9 +77,9 @@ public class FoldersControllerHelper<T> : FilesHelperBase<T>
return await _folderDtoHelper.GetAsync(folder);
}
public async Task<FolderContentDto<T>> GetFolderAsync(T folderId, Guid userIdOrGroupId, FilterType filterType, bool searchInContent, bool withSubFolders)
public async Task<FolderContentDto<T>> GetFolderAsync(T folderId, Guid? userIdOrGroupId, FilterType? filterType, bool? searchInContent, bool? withSubFolders)
{
var folderContentWrapper = await ToFolderContentWrapperAsync(folderId, userIdOrGroupId, filterType, searchInContent, withSubFolders);
var folderContentWrapper = await ToFolderContentWrapperAsync(folderId, userIdOrGroupId ?? Guid.Empty, filterType ?? FilterType.None, searchInContent ?? false, withSubFolders ?? false);
return folderContentWrapper.NotFoundIfNull();
}
@ -175,8 +175,7 @@ public class FoldersControllerHelper<T> : FilesHelperBase<T>
}
var startIndex = Convert.ToInt32(_apiContext.StartIndex);
var items = await _fileStorageService.GetFolderItemsAsync(folderId, startIndex, Convert.ToInt32(_apiContext.Count), filterType,
filterType == FilterType.ByUser, userIdOrGroupId.ToString(), _apiContext.FilterValue, searchInContent, withSubFolders, orderBy);
var items = await _fileStorageService.GetFolderItemsAsync(folderId, startIndex, Convert.ToInt32(_apiContext.Count), filterType, filterType == FilterType.ByUser, userIdOrGroupId.ToString(), _apiContext.FilterValue, searchInContent, withSubFolders, orderBy);
return await _folderContentDtoHelper.GetAsync(items, startIndex);
}

View File

@ -51,9 +51,9 @@ public class VersionController : BaseSettingsController
[AllowAnonymous]
[AllowNotPayment]
[HttpGet("version/build")]
public Task<BuildVersion> GetBuildVersionsAsync()
public async Task<BuildVersion> GetBuildVersionsAsync()
{
return _buildVersion.GetCurrentBuildVersionAsync();
return await _buildVersion.GetCurrentBuildVersionAsync();
}
[HttpGet("version")]

View File

@ -27,82 +27,136 @@
namespace ASC.Web.Api.Controllers.Settings;
public class WebhooksController : BaseSettingsController
{
private readonly DbWorker _webhookDbWorker;
public WebhooksController(
{
private readonly ApiContext _context;
private readonly PermissionContext _permissionContext;
private readonly DbWorker _webhookDbWorker;
private readonly IMapper _mapper;
private readonly WebhookPublisher _webhookPublisher;
public WebhooksController(
ApiContext context,
PermissionContext permissionContext,
ApiContext apiContext,
WebItemManager webItemManager,
IMemoryCache memoryCache,
DbWorker dbWorker,
IHttpContextAccessor httpContextAccessor) : base(apiContext, memoryCache, webItemManager, httpContextAccessor)
{
_webhookDbWorker = dbWorker;
}
IHttpContextAccessor httpContextAccessor,
IMapper mapper,
WebhookPublisher webhookPublisher)
: base(apiContext, memoryCache, webItemManager, httpContextAccessor)
{
_context = context;
_permissionContext = permissionContext;
_webhookDbWorker = dbWorker;
_mapper = mapper;
_webhookPublisher = webhookPublisher;
}
[HttpGet("webhook")]
public async IAsyncEnumerable<WebhooksConfigDto> GetTenantWebhooks()
{
_permissionContext.DemandPermissions(SecutiryConstants.EditPortalSettings);
await foreach (var w in _webhookDbWorker.GetTenantWebhooks())
{
yield return _mapper.Map<WebhooksConfig, WebhooksConfigDto>(w);
}
}
/// <summary>
/// Add new config for webhooks
/// </summary>
[HttpPost("webhook")]
public void CreateWebhook(WebhooksConfig model)
{
if (model.Uri == null)
{
throw new ArgumentNullException("Uri");
}
public async Task<WebhooksConfigDto> CreateWebhook(WebhooksConfigRequestsDto model)
{
_permissionContext.DemandPermissions(SecutiryConstants.EditPortalSettings);
ArgumentNullException.ThrowIfNull(model.Uri);
ArgumentNullException.ThrowIfNull(model.SecretKey);
if (model.SecretKey == null)
{
throw new ArgumentNullException("SecretKey");
}
_webhookDbWorker.AddWebhookConfig(model);
var webhook = await _webhookDbWorker.AddWebhookConfig(model.Name, model.Uri, model.SecretKey);
return _mapper.Map<WebhooksConfig, WebhooksConfigDto>(webhook);
}
/// <summary>
/// Update config for webhooks
/// </summary>
[HttpPut("webhook")]
public void UpdateWebhook(WebhooksConfig model)
{
if (model.Uri == null)
{
throw new ArgumentNullException("Uri");
}
public async Task<WebhooksConfigDto> UpdateWebhook(WebhooksConfigRequestsDto model)
{
_permissionContext.DemandPermissions(SecutiryConstants.EditPortalSettings);
ArgumentNullException.ThrowIfNull(model.Uri);
ArgumentNullException.ThrowIfNull(model.SecretKey);
if (model.SecretKey == null)
{
throw new ArgumentNullException("SecretKey");
}
_webhookDbWorker.UpdateWebhookConfig(model);
var webhook = await _webhookDbWorker.UpdateWebhookConfig(model.Id, model.Name, model.Uri, model.SecretKey, model.Enabled);
return _mapper.Map<WebhooksConfig, WebhooksConfigDto>(webhook);
}
/// <summary>
/// Remove config for webhooks
/// </summary>
[HttpDelete("webhook")]
public void RemoveWebhook(WebhooksConfig model)
{
if (model.Uri == null)
{
throw new ArgumentNullException("Uri");
}
public async Task<WebhooksConfigDto> RemoveWebhook(int id)
{
_permissionContext.DemandPermissions(SecutiryConstants.EditPortalSettings);
if (model.SecretKey == null)
{
throw new ArgumentNullException("SecretKey");
}
_webhookDbWorker.RemoveWebhookConfig(model);
var webhook = await _webhookDbWorker.RemoveWebhookConfig(id);
return _mapper.Map<WebhooksConfig, WebhooksConfigDto>(webhook);
}
/// <summary>
/// Read Webhooks history for actual tenant
/// </summary>
[HttpGet("webhooks")]
public List<WebhooksLog> TenantWebhooks()
{
return _webhookDbWorker.GetTenantWebhooks();
[HttpGet("webhooks/log")]
public async IAsyncEnumerable<WebhooksLogDto> GetJournal(DateTime? delivery, string hookname, string route)
{
_permissionContext.DemandPermissions(SecutiryConstants.EditPortalSettings);
var startIndex = Convert.ToInt32(_context.StartIndex);
var count = Convert.ToInt32(_context.Count);
await foreach (var j in _webhookDbWorker.ReadJournal(startIndex, count, delivery, hookname, route))
{
yield return _mapper.Map<WebhooksLog, WebhooksLogDto>(j);
}
}
[HttpPut("webhook/{id}/retry")]
public async Task<WebhooksLogDto> RetryWebhook(int id)
{
_permissionContext.DemandPermissions(SecutiryConstants.EditPortalSettings);
if (id == 0)
{
throw new ArgumentException(nameof(id));
}
var item = await _webhookDbWorker.ReadJournal(id);
if (item == null)
{
throw new ItemNotFoundException();
}
if (item.Status >= 200 && item.Status <= 299 || item.Status == 0)
{
throw new HttpException(HttpStatusCode.Forbidden);
}
var result = await _webhookPublisher.PublishAsync(item.Method, item.Route, item.RequestPayload, item.ConfigId);
return _mapper.Map<WebhooksLog, WebhooksLogDto>(result);
}
[HttpPut("webhook/retry")]
public async IAsyncEnumerable<WebhooksLogDto> RetryWebhooks(WebhookRetryRequestsDto model)
{
_permissionContext.DemandPermissions(SecutiryConstants.EditPortalSettings);
foreach (var id in model.Ids)
{
var item = await _webhookDbWorker.ReadJournal(id);
if (item == null || item.Status >= 200 && item.Status <= 299 || item.Status == 0)
{
continue;
}
var result = await _webhookPublisher.PublishAsync(item.Method, item.Route, item.RequestPayload, item.ConfigId);
yield return _mapper.Map<WebhooksLog, WebhooksLogDto>(result);
}
}
}

View File

@ -24,29 +24,9 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
namespace ASC.Api.Core.Middleware;
namespace ASC.Web.Api.ApiModels.RequestsDto;
public class DisposeMiddleware
public class WebhookRetryRequestsDto
{
private readonly RequestDelegate _next;
public DisposeMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
context.Response.RegisterForDispose(new DisposableHttpContext(context));
await _next.Invoke(context);
}
public List<int> Ids { get; set; }
}
public static class DisposeMiddlewareExtensions
{
public static IApplicationBuilder UseDisposeMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<DisposeMiddleware>();
}
}

View File

@ -1,53 +1,36 @@
// (c) Copyright Ascensio System SIA 2010-2022
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
namespace ASC.Webhooks.Core.EF.Model;
public class WebhookEntry
{
public int Id { get; set; }
public string Payload { get; set; }
public string SecretKey { get; set; }
public string Uri { get; set; }
public override bool Equals(object other)
{
var toCompareWith = other as WebhookEntry;
if (toCompareWith == null)
{
return false;
}
return Id == toCompareWith.Id &&
Payload == toCompareWith.Payload &&
Uri == toCompareWith.Uri &&
SecretKey == toCompareWith.SecretKey;
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
}
// (c) Copyright Ascensio System SIA 2010-2022
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
namespace ASC.Web.Api.ApiModels.RequestsDto;
public class WebhooksConfigRequestsDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Uri { get; set; }
public string SecretKey { get; set; }
public bool? Enabled { get; set; }
}

View File

@ -1,56 +1,34 @@
// (c) Copyright Ascensio System SIA 2010-2022
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
namespace ASC.Webhooks.Service.Services;
[Singletone]
public class BuildQueueService : BackgroundService
{
internal readonly ConcurrentQueue<WebhookRequest> Queue;
private readonly ICacheNotify<WebhookRequest> _webhookNotify;
public BuildQueueService(ICacheNotify<WebhookRequest> webhookNotify)
{
_webhookNotify = webhookNotify;
Queue = new ConcurrentQueue<WebhookRequest>();
}
public void BuildWebhooksQueue(WebhookRequest request)
{
Queue.Enqueue(request);
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
_webhookNotify.Subscribe(BuildWebhooksQueue, CacheNotifyAction.Update);
stoppingToken.Register(() =>
{
_webhookNotify.Unsubscribe(CacheNotifyAction.Update);
});
return Task.CompletedTask;
}
}
// (c) Copyright Ascensio System SIA 2010-2022
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
namespace ASC.Web.Api.ApiModels.ResponseDto;
public class WebhooksConfigDto : IMapFrom<WebhooksConfig>
{
public string Uri { get; set; }
public string SecretKey { get; set; }
public bool Enabled { get; set; }
}

View File

@ -0,0 +1,42 @@
// (c) Copyright Ascensio System SIA 2010-2022
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
namespace ASC.Web.Api.ApiModels.ResponseDto;
public class WebhooksLogDto : IMapFrom<WebhooksLog>
{
public int Id { get; set; }
public string ConfigName { get; set; }
public DateTime CreationTime { get; set; }
public string Method { get; set; }
public string Route { get; set; }
public string RequestHeaders { get; set; }
public string RequestPayload { get; set; }
public string ResponseHeaders { get; set; }
public string ResponsePayload { get; set; }
public int Status { get; set; }
public DateTime? Delivery { get; set; }
}

View File

@ -29,7 +29,9 @@ namespace ASC.Api.Settings;
[Scope]
public class BuildVersion
{
public string CommunityServer { get; set; }
public string DocSpace { get; set; }
public string CommunityServer { get; set; } //old
public string DocumentServer { get; set; }
@ -54,8 +56,10 @@ public class BuildVersion
}
public async Task<BuildVersion> GetCurrentBuildVersionAsync()
{
CommunityServer = GetCommunityVersion();
{
CommunityServer = "12.0.0";
DocSpace = GetDocSpaceVersion();
DocumentServer = await GetDocumentVersionAsync();
MailServer = GetMailServerVersion();
XmppServer = GetXmppServerVersion();
@ -63,9 +67,9 @@ public class BuildVersion
return this;
}
private string GetCommunityVersion()
private string GetDocSpaceVersion()
{
return _configuration["version:number"] ?? "8.5.0";
return _configuration["version:number"] ?? "1.0.0";
}
private Task<string> GetDocumentVersionAsync()

View File

@ -62,6 +62,7 @@ global using ASC.Common.Radicale.Core;
global using ASC.Common.Security.Authorizing;
global using ASC.Common.Threading;
global using ASC.Common.Utils;
global using ASC.Common.Web;
global using ASC.Core;
global using ASC.Core.Billing;
global using ASC.Core.Common.Configuration;

View File

@ -1,93 +0,0 @@
// (c) Copyright Ascensio System SIA 2010-2022
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
namespace ASC.Web.Core.Helpers;
[Scope]
public class AuthorizationHelper
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserManager _userManager;
private readonly SecurityContext _securityContext;
private readonly PasswordHasher _passwordHasher;
public AuthorizationHelper(
IHttpContextAccessor httpContextAccessor,
UserManager userManager,
SecurityContext securityContext,
PasswordHasher passwordHasher)
{
_httpContextAccessor = httpContextAccessor;
_userManager = userManager;
_securityContext = securityContext;
_passwordHasher = passwordHasher;
}
public bool ProcessBasicAuthorization(out string authCookie)
{
authCookie = null;
try
{
//Try basic
var authorization = _httpContextAccessor.HttpContext.Request.Cookies["asc_auth_key"] ?? _httpContextAccessor.HttpContext.Request.Headers["Authorization"].ToString();
if (string.IsNullOrEmpty(authorization))
{
return false;
}
authorization = authorization.Trim();
if (0 <= authorization.IndexOf("Basic", 0))
{
var arr = Encoding.ASCII.GetString(Convert.FromBase64String(authorization.Substring(6))).Split(new[] { ':' });
var username = arr[0];
var password = arr[1];
var u = _userManager.GetUserByEmail(username);
if (u != null && u.Id != ASC.Core.Users.Constants.LostUser.Id)
{
var passwordHash = _passwordHasher.GetClientPassword(password);
authCookie = _securityContext.AuthenticateMe(u.Email, passwordHash);
}
}
else if (0 <= authorization.IndexOf("Bearer", 0))
{
authorization = authorization.Substring("Bearer ".Length);
if (_securityContext.AuthenticateMe(authorization))
{
authCookie = authorization;
}
}
else
{
if (_securityContext.AuthenticateMe(authorization))
{
authCookie = authorization;
}
}
}
catch (Exception) { }
return _securityContext.IsAuthenticated;
}
}