1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-27 14:16:19 -05:00

Merge branch 'main' into billing/license-refactor

This commit is contained in:
Conner Turnbull 2025-06-26 09:20:19 -04:00
commit 43f31a312b
No known key found for this signature in database
20 changed files with 171 additions and 33 deletions

View File

@ -59,6 +59,8 @@ jobs:
name: Create GitHub release
runs-on: ubuntu-22.04
needs: setup
permissions:
contents: write
steps:
- name: Download latest release Docker stubs
if: ${{ inputs.release_type != 'Dry Run' }}

View File

@ -59,6 +59,7 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gosu \
curl \
krb5-user \
&& rm -rf /var/lib/apt/lists/*
# Copy app from the build stage

View File

@ -4,7 +4,6 @@ using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Models.Response;
using Bit.Api.Models.Response;
using Bit.Api.Vault.Models.Response;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Services;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
@ -72,7 +71,7 @@ public class EmergencyAccessController : Controller
{
var user = await _userService.GetUserByPrincipalAsync(User);
var policies = await _emergencyAccessService.GetPoliciesAsync(id, user);
var responses = policies.Select<Policy, PolicyResponseModel>(policy => new PolicyResponseModel(policy));
var responses = policies?.Select(policy => new PolicyResponseModel(policy));
return new ListResponseModel<PolicyResponseModel>(responses);
}

View File

@ -2,4 +2,4 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record SlackIntegration(string token);
public record SlackIntegration(string Token);

View File

@ -2,4 +2,4 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record SlackIntegrationConfiguration(string channelId);
public record SlackIntegrationConfiguration(string ChannelId);

View File

@ -2,4 +2,4 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record SlackIntegrationConfigurationDetails(string channelId, string token);
public record SlackIntegrationConfigurationDetails(string ChannelId, string Token);

View File

@ -2,4 +2,4 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record WebhookIntegrationConfiguration(string url);
public record WebhookIntegrationConfiguration(string Url, string? Scheme = null, string? Token = null);

View File

@ -2,4 +2,4 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record WebhookIntegrationConfigurationDetails(string url);
public record WebhookIntegrationConfigurationDetails(string Url, string? Scheme = null, string? Token = null);

View File

@ -11,9 +11,9 @@ public class SlackIntegrationHandler(
public override async Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<SlackIntegrationConfigurationDetails> message)
{
await slackService.SendSlackMessageByChannelIdAsync(
message.Configuration.token,
message.Configuration.Token,
message.RenderedTemplate,
message.Configuration.channelId
message.Configuration.ChannelId
);
return new IntegrationHandlerResult(success: true, message: message);

View File

@ -2,6 +2,7 @@
using System.Globalization;
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
@ -20,8 +21,16 @@ public class WebhookIntegrationHandler(
public override async Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{
var content = new StringContent(message.RenderedTemplate, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(message.Configuration.url, content);
var request = new HttpRequestMessage(HttpMethod.Post, message.Configuration.Url);
request.Content = new StringContent(message.RenderedTemplate, Encoding.UTF8, "application/json");
if (!string.IsNullOrEmpty(message.Configuration.Scheme))
{
request.Headers.Authorization = new AuthenticationHeaderValue(
scheme: message.Configuration.Scheme,
parameter: message.Configuration.Token
);
}
var response = await _httpClient.SendAsync(request);
var result = new IntegrationHandlerResult(success: response.IsSuccessStatusCode, message);
switch (response.StatusCode)

View File

@ -34,6 +34,7 @@ public class OrganizationSale
var subscriptionSetup = GetSubscriptionSetup(signup);
subscriptionSetup.SkipTrial = signup.SkipTrial;
subscriptionSetup.InitiationPath = signup.InitiationPath;
return new OrganizationSale
{

View File

@ -10,6 +10,7 @@ public class SubscriptionSetup
public required PasswordManager PasswordManagerOptions { get; set; }
public SecretsManager? SecretsManagerOptions { get; set; }
public bool SkipTrial = false;
public string? InitiationPath { get; set; }
public class PasswordManager
{

View File

@ -420,7 +420,11 @@ public class OrganizationBillingService(
Items = subscriptionItemOptionsList,
Metadata = new Dictionary<string, string>
{
["organizationId"] = organizationId.ToString()
["organizationId"] = organizationId.ToString(),
["trialInitiationPath"] = !string.IsNullOrEmpty(subscriptionSetup.InitiationPath) &&
subscriptionSetup.InitiationPath.Contains("trial from marketing website")
? "marketing-initiated"
: "product-initiated"
},
OffSession = true,
TrialPeriodDays = subscriptionSetup.SkipTrial ? 0 : plan.TrialPeriodDays

View File

@ -194,7 +194,6 @@ public static class FeatureFlagKeys
public const string IpcChannelFramework = "ipc-channel-framework";
/* Tools Team */
public const string ItemShare = "item-share";
public const string DesktopSendUIRefresh = "desktop-send-ui-refresh";
/* Vault Team */

View File

@ -151,7 +151,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Slack;
var slackConfig = new SlackIntegrationConfiguration(channelId: "C123456");
var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456");
model.Configuration = JsonSerializer.Serialize(slackConfig);
model.Template = "Template String";
@ -188,7 +188,44 @@ public class OrganizationIntegrationsConfigurationControllerTests
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(url: "https://localhost");
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration);
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(organizationIntegrationConfiguration);
var requestAction = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(requestAction);
Assert.Equal(expected.Id, requestAction.Id);
Assert.Equal(expected.Configuration, requestAction.Configuration);
Assert.Equal(expected.EventType, requestAction.EventType);
Assert.Equal(expected.Template, requestAction.Template);
}
[Theory, BitAutoData]
public async Task PostAsync_OnlyUrlProvided_Webhook_Succeeds(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
@ -350,7 +387,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(url: "https://localhost");
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = null;
@ -393,7 +430,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
organizationIntegration.Type = IntegrationType.Slack;
var slackConfig = new SlackIntegrationConfiguration(channelId: "C123456");
var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456");
model.Configuration = JsonSerializer.Serialize(slackConfig);
model.Template = "Template String";
@ -436,7 +473,49 @@ public class OrganizationIntegrationsConfigurationControllerTests
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(url: "https://localhost");
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration));
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegrationConfiguration);
var requestAction = await sutProvider.Sut.UpdateAsync(
organizationId,
organizationIntegration.Id,
organizationIntegrationConfiguration.Id,
model);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(requestAction);
Assert.Equal(expected.Id, requestAction.Id);
Assert.Equal(expected.Configuration, requestAction.Configuration);
Assert.Equal(expected.EventType, requestAction.EventType);
Assert.Equal(expected.Template, requestAction.Template);
}
[Theory, BitAutoData]
public async Task UpdateAsync_OnlyUrlProvided_Webhook_Succeeds(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
@ -476,7 +555,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(url: "https://localhost");
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
@ -582,7 +661,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
organizationIntegration.Type = IntegrationType.Slack;
var slackConfig = new SlackIntegrationConfiguration(channelId: "C123456");
var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456");
model.Configuration = JsonSerializer.Serialize(slackConfig);
model.Template = null;

View File

@ -43,7 +43,7 @@ public class OrganizationIntegrationConfigurationRequestModelTests
[InlineData(" ")]
public void IsValidForType_EmptyTemplate_ReturnsFalse(string? template)
{
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com"));
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com", "Bearer", "AUTH-TOKEN"));
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
@ -92,7 +92,7 @@ public class OrganizationIntegrationConfigurationRequestModelTests
}
[Fact]
public void IsValidForType_ValidWebhookConfiguration_ReturnsTrue()
public void IsValidForType_ValidNoAuthWebhookConfiguration_ReturnsTrue()
{
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com"));
var model = new OrganizationIntegrationConfigurationRequestModel
@ -104,6 +104,19 @@ public class OrganizationIntegrationConfigurationRequestModelTests
Assert.True(model.IsValidForType(IntegrationType.Webhook));
}
[Fact]
public void IsValidForType_ValidWebhookConfiguration_ReturnsTrue()
{
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com", "Bearer", "AUTH-TOKEN"));
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = "template"
};
Assert.True(model.IsValidForType(IntegrationType.Webhook));
}
[Fact]
public void IsValidForType_UnknownIntegrationType_ReturnsFalse()
{

View File

@ -14,7 +14,7 @@ public class IntegrationMessageTests
{
var message = new IntegrationMessage<WebhookIntegrationConfigurationDetails>
{
Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"),
Configuration = new WebhookIntegrationConfigurationDetails("https://localhost", "Bearer", "AUTH-TOKEN"),
MessageId = _messageId,
RetryCount = 2,
RenderedTemplate = string.Empty,
@ -34,7 +34,7 @@ public class IntegrationMessageTests
{
var message = new IntegrationMessage<WebhookIntegrationConfigurationDetails>
{
Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"),
Configuration = new WebhookIntegrationConfigurationDetails("https://localhost", "Bearer", "AUTH-TOKEN"),
MessageId = _messageId,
RenderedTemplate = "This is the message",
IntegrationType = IntegrationType.Webhook,

View File

@ -62,7 +62,7 @@ public class EventIntegrationHandlerTests
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config.Configuration = null;
config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url });
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url });
config.Template = template;
return [config];
@ -72,11 +72,11 @@ public class EventIntegrationHandlerTests
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config.Configuration = null;
config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url });
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url });
config.Template = template;
var config2 = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config2.Configuration = null;
config2.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url2 });
config2.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url2 });
config2.Template = template;
return [config, config2];

View File

@ -14,7 +14,7 @@ public class IntegrationHandlerTests
var sut = new TestIntegrationHandler();
var expected = new IntegrationMessage<WebhookIntegrationConfigurationDetails>()
{
Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"),
Configuration = new WebhookIntegrationConfigurationDetails("https://localhost", "Bearer", "AUTH-TOKEN"),
MessageId = "TestMessageId",
IntegrationType = IntegrationType.Webhook,
RenderedTemplate = "Template",

View File

@ -1,4 +1,5 @@
using System.Net;
using System.Net.Http.Headers;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
@ -16,6 +17,8 @@ public class WebhookIntegrationHandlerTests
{
private readonly MockedHttpMessageHandler _handler;
private readonly HttpClient _httpClient;
private const string _scheme = "Bearer";
private const string _token = "AUTH_TOKEN";
private const string _webhookUrl = "http://localhost/test/event";
public WebhookIntegrationHandlerTests()
@ -39,7 +42,7 @@ public class WebhookIntegrationHandlerTests
}
[Theory, BitAutoData]
public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
public async Task HandleAsync_SuccessfulRequestWithoutAuth_ReturnsSuccess(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
@ -59,6 +62,33 @@ public class WebhookIntegrationHandlerTests
var returned = await request.Content.ReadAsStringAsync();
Assert.Equal(HttpMethod.Post, request.Method);
Assert.Null(request.Headers.Authorization);
Assert.Equal(_webhookUrl, request.RequestUri.ToString());
AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned);
}
[Theory, BitAutoData]
public async Task HandleAsync_SuccessfulRequestWithAuthorizationHeader_ReturnsSuccess(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token);
var result = await sutProvider.Sut.HandleAsync(message);
Assert.True(result.Success);
Assert.Equal(result.Message, message);
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName))
);
Assert.Single(_handler.CapturedRequests);
var request = _handler.CapturedRequests[0];
Assert.NotNull(request);
var returned = await request.Content.ReadAsStringAsync();
Assert.Equal(HttpMethod.Post, request.Method);
Assert.Equal(new AuthenticationHeaderValue(_scheme, _token), request.Headers.Authorization);
Assert.Equal(_webhookUrl, request.RequestUri.ToString());
AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned);
}
@ -71,7 +101,7 @@ public class WebhookIntegrationHandlerTests
var retryAfter = now.AddSeconds(60);
sutProvider.GetDependency<FakeTimeProvider>().SetUtcNow(now);
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token);
_handler.Fallback
.WithStatusCode(HttpStatusCode.TooManyRequests)
@ -94,7 +124,7 @@ public class WebhookIntegrationHandlerTests
var sutProvider = GetSutProvider();
var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);
var retryAfter = now.AddSeconds(60);
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token);
_handler.Fallback
.WithStatusCode(HttpStatusCode.TooManyRequests)
@ -115,7 +145,7 @@ public class WebhookIntegrationHandlerTests
public async Task HandleAsync_InternalServerError_ReturnsFailureSetsRetryable(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token);
_handler.Fallback
.WithStatusCode(HttpStatusCode.InternalServerError)
@ -134,7 +164,7 @@ public class WebhookIntegrationHandlerTests
public async Task HandleAsync_UnexpectedRedirect_ReturnsFailureNotRetryable(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token);
_handler.Fallback
.WithStatusCode(HttpStatusCode.TemporaryRedirect)