Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit3956e81

Browse files
committed
Merge branch 'main' into billing/PM-28100/2019-families-email
# Conflicts:#src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs#test/Billing.Test/Services/UpcomingInvoiceHandlerTests.cs
2 parents75ef19d +1334ed8 commit3956e81

File tree

18 files changed

+2248
-382
lines changed

18 files changed

+2248
-382
lines changed

‎.github/workflows/build.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ jobs:
280280
output-format:sarif
281281

282282
-name:Upload Grype results to GitHub
283-
uses:github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee# v4.31.2
283+
uses:github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b# v4.31.4
284284
with:
285285
sarif_file:${{ steps.container-scan.outputs.sarif }}
286286
sha:${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}

‎src/Api/AdminConsole/Controllers/OrganizationsController.cs‎

Lines changed: 19 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
usingBit.Api.Models.Request.Organizations;
1313
usingBit.Api.Models.Response;
1414
usingBit.Core;
15-
usingBit.Core.AdminConsole.Entities;
1615
usingBit.Core.AdminConsole.Enums;
1716
usingBit.Core.AdminConsole.Models.Business.Tokenables;
1817
usingBit.Core.AdminConsole.Models.Data.Organizations.Policies;
@@ -70,6 +69,7 @@ public class OrganizationsController : Controller
7069
privatereadonlyIPolicyRequirementQuery_policyRequirementQuery;
7170
privatereadonlyIPricingClient_pricingClient;
7271
privatereadonlyIOrganizationUpdateKeysCommand_organizationUpdateKeysCommand;
72+
privatereadonlyIOrganizationUpdateCommand_organizationUpdateCommand;
7373

7474
publicOrganizationsController(
7575
IOrganizationRepositoryorganizationRepository,
@@ -94,7 +94,8 @@ public OrganizationsController(
9494
IOrganizationDeleteCommandorganizationDeleteCommand,
9595
IPolicyRequirementQuerypolicyRequirementQuery,
9696
IPricingClientpricingClient,
97-
IOrganizationUpdateKeysCommandorganizationUpdateKeysCommand)
97+
IOrganizationUpdateKeysCommandorganizationUpdateKeysCommand,
98+
IOrganizationUpdateCommandorganizationUpdateCommand)
9899
{
99100
_organizationRepository=organizationRepository;
100101
_organizationUserRepository=organizationUserRepository;
@@ -119,6 +120,7 @@ public OrganizationsController(
119120
_policyRequirementQuery=policyRequirementQuery;
120121
_pricingClient=pricingClient;
121122
_organizationUpdateKeysCommand=organizationUpdateKeysCommand;
123+
_organizationUpdateCommand=organizationUpdateCommand;
122124
}
123125

124126
[HttpGet("{id}")]
@@ -224,36 +226,31 @@ public async Task<OrganizationResponseModel> CreateWithoutPaymentAsync([FromBody
224226
returnnewOrganizationResponseModel(result.Organization,plan);
225227
}
226228

227-
[HttpPut("{id}")]
228-
publicasyncTask<OrganizationResponseModel>Put(stringid,[FromBody]OrganizationUpdateRequestModelmodel)
229+
[HttpPut("{organizationId:guid}")]
230+
publicasyncTask<IResult>Put(GuidorganizationId,[FromBody]OrganizationUpdateRequestModelmodel)
229231
{
230-
varorgIdGuid=newGuid(id);
232+
// If billing email is being changed, require subscription editing permissions.
233+
// Otherwise, organization owner permissions are sufficient.
234+
varrequiresBillingPermission=model.BillingEmailis notnull;
235+
varauthorized=requiresBillingPermission
236+
?await_currentContext.EditSubscription(organizationId)
237+
:await_currentContext.OrganizationOwner(organizationId);
231238

232-
varorganization=await_organizationRepository.GetByIdAsync(orgIdGuid);
233-
if(organization==null)
239+
if(!authorized)
234240
{
235-
thrownewNotFoundException();
241+
returnTypedResults.Unauthorized();
236242
}
237243

238-
varupdateBilling=ShouldUpdateBilling(model,organization);
239-
240-
varhasRequiredPermissions=updateBilling
241-
?await_currentContext.EditSubscription(orgIdGuid)
242-
:await_currentContext.OrganizationOwner(orgIdGuid);
243-
244-
if(!hasRequiredPermissions)
245-
{
246-
thrownewNotFoundException();
247-
}
244+
varcommandRequest=model.ToCommandRequest(organizationId);
245+
varupdatedOrganization=await_organizationUpdateCommand.UpdateAsync(commandRequest);
248246

249-
await_organizationService.UpdateAsync(model.ToOrganization(organization,_globalSettings),updateBilling);
250-
varplan=await_pricingClient.GetPlan(organization.PlanType);
251-
returnnewOrganizationResponseModel(organization,plan);
247+
varplan=await_pricingClient.GetPlan(updatedOrganization.PlanType);
248+
returnTypedResults.Ok(newOrganizationResponseModel(updatedOrganization,plan));
252249
}
253250

254251
[HttpPost("{id}")]
255252
[Obsolete("This endpoint is deprecated. Use PUT method instead")]
256-
publicasyncTask<OrganizationResponseModel>PostPut(stringid,[FromBody]OrganizationUpdateRequestModelmodel)
253+
publicasyncTask<IResult>PostPut(Guidid,[FromBody]OrganizationUpdateRequestModelmodel)
257254
{
258255
returnawaitPut(id,model);
259256
}
@@ -588,11 +585,4 @@ public async Task<PlanType> GetPlanType(string id)
588585

589586
returnorganization.PlanType;
590587
}
591-
592-
privateboolShouldUpdateBilling(OrganizationUpdateRequestModelmodel,Organizationorganization)
593-
{
594-
varorganizationNameChanged=model.Name!=organization.Name;
595-
varbillingEmailChanged=model.BillingEmail!=organization.BillingEmail;
596-
return!_globalSettings.SelfHosted&&(organizationNameChanged||billingEmailChanged);
597-
}
598588
}
Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,28 @@
1-
// FIXME: Update this file to be null safe and then delete the line below
2-
#nullable disable
3-
4-
usingSystem.ComponentModel.DataAnnotations;
1+
usingSystem.ComponentModel.DataAnnotations;
52
usingSystem.Text.Json.Serialization;
6-
usingBit.Core.AdminConsole.Entities;
7-
usingBit.Core.Models.Data;
8-
usingBit.Core.Settings;
3+
usingBit.Core.AdminConsole.OrganizationFeatures.Organizations.Update;
94
usingBit.Core.Utilities;
105

116
namespaceBit.Api.AdminConsole.Models.Request.Organizations;
127

138
publicclassOrganizationUpdateRequestModel
149
{
15-
[Required]
1610
[StringLength(50,ErrorMessage="The field Name exceeds the maximum length.")]
1711
[JsonConverter(typeof(HtmlEncodingStringConverter))]
18-
publicstringName{get;set;}
19-
[StringLength(50,ErrorMessage="The field Business Name exceeds the maximum length.")]
20-
[JsonConverter(typeof(HtmlEncodingStringConverter))]
21-
publicstringBusinessName{get;set;}
12+
publicstring?Name{get;set;}
13+
2214
[EmailAddress]
23-
[Required]
2415
[StringLength(256)]
25-
publicstringBillingEmail{get;set;}
26-
publicPermissionsPermissions{get;set;}
27-
publicOrganizationKeysRequestModelKeys{get;set;}
16+
publicstring?BillingEmail{get;set;}
17+
18+
publicOrganizationKeysRequestModel?Keys{get;set;}
2819

29-
publicvirtualOrganizationToOrganization(OrganizationexistingOrganization,GlobalSettingsglobalSettings)
20+
publicOrganizationUpdateRequestToCommandRequest(GuidorganizationId)=>new()
3021
{
31-
if(!globalSettings.SelfHosted)
32-
{
33-
// These items come from the license file
34-
existingOrganization.Name=Name;
35-
existingOrganization.BusinessName=BusinessName;
36-
existingOrganization.BillingEmail=BillingEmail?.ToLowerInvariant()?.Trim();
37-
}
38-
Keys?.ToOrganization(existingOrganization);
39-
returnexistingOrganization;
40-
}
22+
OrganizationId=organizationId,
23+
Name=Name,
24+
BillingEmail=BillingEmail,
25+
PublicKey=Keys?.PublicKey,
26+
EncryptedPrivateKey=Keys?.EncryptedPrivateKey
27+
};
4128
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
usingBit.Core.AdminConsole.Entities;
2+
usingBit.Core.AdminConsole.OrganizationFeatures.Organizations.Update;
3+
4+
namespaceBit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
5+
6+
publicinterfaceIOrganizationUpdateCommand
7+
{
8+
/// <summary>
9+
/// Updates an organization's information in the Bitwarden database and Stripe (if required).
10+
/// Also optionally updates an organization's public-private keypair if it was not created with one.
11+
/// On self-host, only the public-private keys will be updated because all other properties are fixed by the license file.
12+
/// </summary>
13+
/// <param name="request">The update request containing the details to be updated.</param>
14+
Task<Organization>UpdateAsync(OrganizationUpdateRequestrequest);
15+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
usingBit.Core.AdminConsole.Entities;
2+
usingBit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
3+
usingBit.Core.Billing.Organizations.Services;
4+
usingBit.Core.Enums;
5+
usingBit.Core.Exceptions;
6+
usingBit.Core.Repositories;
7+
usingBit.Core.Services;
8+
usingBit.Core.Settings;
9+
10+
namespaceBit.Core.AdminConsole.OrganizationFeatures.Organizations.Update;
11+
12+
publicclassOrganizationUpdateCommand(
13+
IOrganizationServiceorganizationService,
14+
IOrganizationRepositoryorganizationRepository,
15+
IGlobalSettingsglobalSettings,
16+
IOrganizationBillingServiceorganizationBillingService
17+
):IOrganizationUpdateCommand
18+
{
19+
publicasyncTask<Organization>UpdateAsync(OrganizationUpdateRequestrequest)
20+
{
21+
varorganization=awaitorganizationRepository.GetByIdAsync(request.OrganizationId);
22+
if(organization==null)
23+
{
24+
thrownewNotFoundException();
25+
}
26+
27+
if(globalSettings.SelfHosted)
28+
{
29+
returnawaitUpdateSelfHostedAsync(organization,request);
30+
}
31+
32+
returnawaitUpdateCloudAsync(organization,request);
33+
}
34+
35+
privateasyncTask<Organization>UpdateCloudAsync(Organizationorganization,OrganizationUpdateRequestrequest)
36+
{
37+
// Store original values for comparison
38+
varoriginalName=organization.Name;
39+
varoriginalBillingEmail=organization.BillingEmail;
40+
41+
// Apply updates to organization
42+
organization.UpdateDetails(request);
43+
organization.BackfillPublicPrivateKeys(request);
44+
awaitorganizationService.ReplaceAndUpdateCacheAsync(organization,EventType.Organization_Updated);
45+
46+
// Update billing information in Stripe if required
47+
awaitUpdateBillingAsync(organization,originalName,originalBillingEmail);
48+
49+
returnorganization;
50+
}
51+
52+
/// <summary>
53+
/// Self-host cannot update the organization details because they are set by the license file.
54+
/// However, this command does offer a soft migration pathway for organizations without public and private keys.
55+
/// If we remove this migration code in the future, this command and endpoint can become cloud only.
56+
/// </summary>
57+
privateasyncTask<Organization>UpdateSelfHostedAsync(Organizationorganization,OrganizationUpdateRequestrequest)
58+
{
59+
organization.BackfillPublicPrivateKeys(request);
60+
awaitorganizationService.ReplaceAndUpdateCacheAsync(organization,EventType.Organization_Updated);
61+
returnorganization;
62+
}
63+
64+
privateasyncTaskUpdateBillingAsync(Organizationorganization,stringoriginalName,string?originalBillingEmail)
65+
{
66+
// Update Stripe if name or billing email changed
67+
varshouldUpdateBilling=originalName!=organization.Name||
68+
originalBillingEmail!=organization.BillingEmail;
69+
70+
if(!shouldUpdateBilling||string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
71+
{
72+
return;
73+
}
74+
75+
awaitorganizationBillingService.UpdateOrganizationNameAndEmail(organization);
76+
}
77+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
usingBit.Core.AdminConsole.Entities;
2+
3+
namespaceBit.Core.AdminConsole.OrganizationFeatures.Organizations.Update;
4+
5+
publicstaticclassOrganizationUpdateExtensions
6+
{
7+
/// <summary>
8+
/// Updates the organization name and/or billing email.
9+
/// Any null property on the request object will be skipped.
10+
/// </summary>
11+
publicstaticvoidUpdateDetails(thisOrganizationorganization,OrganizationUpdateRequestrequest)
12+
{
13+
// These values may or may not be sent by the client depending on the operation being performed.
14+
// Skip any values not provided.
15+
if(request.Nameis notnull)
16+
{
17+
organization.Name=request.Name;
18+
}
19+
20+
if(request.BillingEmailis notnull)
21+
{
22+
organization.BillingEmail=request.BillingEmail.ToLowerInvariant().Trim();
23+
}
24+
}
25+
26+
/// <summary>
27+
/// Updates the organization public and private keys if provided and not already set.
28+
/// This is legacy code for old organizations that were not created with a public/private keypair. It is a soft
29+
/// migration that will silently migrate organizations when they change their details.
30+
/// </summary>
31+
publicstaticvoidBackfillPublicPrivateKeys(thisOrganizationorganization,OrganizationUpdateRequestrequest)
32+
{
33+
if(!string.IsNullOrWhiteSpace(request.PublicKey)&&string.IsNullOrWhiteSpace(organization.PublicKey))
34+
{
35+
organization.PublicKey=request.PublicKey;
36+
}
37+
38+
if(!string.IsNullOrWhiteSpace(request.EncryptedPrivateKey)&&string.IsNullOrWhiteSpace(organization.PrivateKey))
39+
{
40+
organization.PrivateKey=request.EncryptedPrivateKey;
41+
}
42+
}
43+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
namespaceBit.Core.AdminConsole.OrganizationFeatures.Organizations.Update;
2+
3+
/// <summary>
4+
/// Request model for updating the name, billing email, and/or public-private keys for an organization (legacy migration code).
5+
/// Any combination of these properties can be updated, so they are optional. If none are specified it will not update anything.
6+
/// </summary>
7+
publicrecordOrganizationUpdateRequest
8+
{
9+
/// <summary>
10+
/// The ID of the organization to update.
11+
/// </summary>
12+
publicrequiredGuidOrganizationId{get;init;}
13+
14+
/// <summary>
15+
/// The new organization name to apply (optional, this is skipped if not provided).
16+
/// </summary>
17+
publicstring?Name{get;init;}
18+
19+
/// <summary>
20+
/// The new billing email address to apply (optional, this is skipped if not provided).
21+
/// </summary>
22+
publicstring?BillingEmail{get;init;}
23+
24+
/// <summary>
25+
/// The organization's public key to set (optional, only set if not already present on the organization).
26+
/// </summary>
27+
publicstring?PublicKey{get;init;}
28+
29+
/// <summary>
30+
/// The organization's encrypted private key to set (optional, only set if not already present on the organization).
31+
/// </summary>
32+
publicstring?EncryptedPrivateKey{get;init;}
33+
}

‎src/Core/Billing/Organizations/Services/IOrganizationBillingService.cs‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,15 @@ Task UpdatePaymentMethod(
5656
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="organization"/> is <see langword="null"/>.</exception>
5757
/// <exception cref="BillingException">Thrown when no payment method is found for the customer, no plan IDs are provided, or subscription update fails.</exception>
5858
TaskUpdateSubscriptionPlanFrequency(Organizationorganization,PlanTypenewPlanType);
59+
60+
/// <summary>
61+
/// Updates the organization name and email on the Stripe customer entry.
62+
/// This only updates Stripe, not the Bitwarden database.
63+
/// </summary>
64+
/// <remarks>
65+
/// The caller should ensure that the organization has a GatewayCustomerId before calling this method.
66+
/// </remarks>
67+
/// <param name="organization">The organization to update in Stripe.</param>
68+
/// <exception cref="BillingException">Thrown when the organization does not have a GatewayCustomerId.</exception>
69+
TaskUpdateOrganizationNameAndEmail(Organizationorganization);
5970
}

‎src/Core/Billing/Organizations/Services/OrganizationBillingService.cs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,35 @@ public async Task UpdateSubscriptionPlanFrequency(
176176
}
177177
}
178178

179+
publicasyncTaskUpdateOrganizationNameAndEmail(Organizationorganization)
180+
{
181+
if(organization.GatewayCustomerIdisnull)
182+
{
183+
thrownewBillingException("Cannot update an organization in Stripe without a GatewayCustomerId.");
184+
}
185+
186+
varnewDisplayName=organization.DisplayName();
187+
188+
awaitstripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId,
189+
newCustomerUpdateOptions
190+
{
191+
Email=organization.BillingEmail,
192+
Description=newDisplayName,
193+
InvoiceSettings=newCustomerInvoiceSettingsOptions
194+
{
195+
// This overwrites the existing custom fields for this organization
196+
CustomFields=[
197+
newCustomerInvoiceSettingsCustomFieldOptions
198+
{
199+
Name=organization.SubscriberType(),
200+
Value=newDisplayName.Length<=30
201+
?newDisplayName
202+
:newDisplayName[..30]
203+
}]
204+
},
205+
});
206+
}
207+
179208
#region Utilities
180209

181210
privateasyncTask<Customer>CreateCustomerAsync(

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp