Skip to content

Commit 6858116

Browse files
committed
Merge branch 'main' into billing/PM-36951/cohorts-crud-ui
# Conflicts: # src/Core/Billing/Extensions/ServiceCollectionExtensions.cs
2 parents aaa1c03 + 967be3e commit 6858116

52 files changed

Lines changed: 4227 additions & 137 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/review-code.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Code Review
22

33
on:
44
pull_request:
5-
types: [opened, synchronize, reopened]
5+
types: [labeled, opened, ready_for_review, reopened, synchronize]
66

77
permissions: {}
88

AppHost/AppHost.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@
1313
builder.ConfigureIdp();
1414
var services = builder.ConfigureServices(db, secretsSetup, mail, azurite);
1515

16-
#if ENABLE_NODEJS_COMMUNITY_PLUGIN
1716
builder.ConfigureWebFrontend(services["api"]);
18-
#endif
1917

2018
#if ENABLE_NGROK_COMMUNITY_PLUGIN
2119
builder.ConfigureNgrok((services["billing"], "http"));

AppHost/AppHost.csproj

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
<!-- Disable community plugins by default -->
1212
<EnableNgrokCommunityPlugin>false</EnableNgrokCommunityPlugin>
1313
<DefineConstants Condition="'$(EnableNgrokCommunityPlugin)' == 'true'">$(DefineConstants);ENABLE_NGROK_COMMUNITY_PLUGIN</DefineConstants>
14-
<EnableNodeJsCommunityPlugin>false</EnableNodeJsCommunityPlugin>
15-
<DefineConstants Condition="'$(EnableNodeJsCommunityPlugin)' == 'true'">$(DefineConstants);ENABLE_NODEJS_COMMUNITY_PLUGIN</DefineConstants>
1614
</PropertyGroup>
1715

1816
<ItemGroup>
@@ -26,10 +24,6 @@
2624
<PackageReference Include="CommunityToolkit.Aspire.Hosting.Ngrok" Version="[13.3.0]" />
2725
</ItemGroup>
2826

29-
<ItemGroup Condition="'$(EnableNodeJsCommunityPlugin)' == 'true'">
30-
<PackageReference Include="CommunityToolkit.Aspire.Hosting.NodeJS.Extensions" Version="[9.9.0]"/>
31-
</ItemGroup>
32-
3327
<ItemGroup>
3428
<ProjectReference Include="..\src\Admin\Admin.csproj"/>
3529
<ProjectReference Include="..\src\Api\Api.csproj"/>

AppHost/BuilderExtensions.cs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Aspire.Hosting.Azure;
2+
using Aspire.Hosting.JavaScript;
23
using Azure.Provisioning;
34
using Azure.Provisioning.Storage;
45

@@ -66,11 +67,14 @@ public static IResourceBuilder<AzureStorageResource> ConfigureAzurite(this IDist
6667
MaxAgeInSeconds = new BicepValue<int>("30")
6768
}));
6869
})
69-
.RunAsEmulator(c =>
70+
.RunAsEmulator(emulator =>
7071
{
71-
c.WithBlobPort(10000)
72+
emulator
73+
.WithBlobPort(10000)
7274
.WithQueuePort(10001)
73-
.WithTablePort(10002);
75+
.WithTablePort(10002)
76+
.WithDataVolume()
77+
.WithLifetime(ContainerLifetime.Persistent);
7478
});
7579

7680
builder
@@ -256,30 +260,29 @@ private static (string Name, string Tag) GetImageParts(this IDistributedApplicat
256260
private static bool IsSelfHosted(this IDistributedApplicationBuilder builder) =>
257261
builder.Configuration["SelfHost"]?.Equals("true", StringComparison.OrdinalIgnoreCase) == true;
258262

259-
#if ENABLE_NODEJS_COMMUNITY_PLUGIN
260263
public static void ConfigureWebFrontend(this IDistributedApplicationBuilder builder,
261264
IResourceBuilder<ProjectResource> api)
262265
{
263266
if (!int.TryParse(builder.Required("WebFrontend:Port"), out var port))
264267
throw new InvalidOperationException("Invalid value for WebFrontend:Port.");
265268

266269
builder
267-
.AddBitwardenNpmApp("web-frontend", "web", api)
268-
.WithHttpsEndpoint(port, port, "angular-http", isProxied: false)
270+
.AddBitwardenNpmApp("web-frontend", "web", api, port: port)
269271
.WithUrl(builder.Required("WebFrontend:Url"))
270272
.WithExternalHttpEndpoints();
271273
}
272274

273-
private static IResourceBuilder<NodeAppResource> AddBitwardenNpmApp(this IDistributedApplicationBuilder builder,
274-
string name, string path, IResourceBuilder<ProjectResource> api, string scriptName = "build:bit:watch")
275+
private static IResourceBuilder<JavaScriptAppResource> AddBitwardenNpmApp(this IDistributedApplicationBuilder builder,
276+
string name, string path, IResourceBuilder<ProjectResource> api, int port, string scriptName = "build:bit:watch")
275277
{
276278
return builder
277-
.AddNpmApp(name, $"{builder.Required("ClientsPath")}/{path}", scriptName)
279+
.AddJavaScriptApp(name, $"{builder.Required("ClientsPath")}/{path}", scriptName)
280+
.WithHttpsEndpoint(port, port, "angular-http", isProxied: false)
281+
.WithNpm(install: false)
278282
.WithReference(api)
279283
.WaitFor(api)
280284
.WithExplicitStart();
281285
}
282-
#endif
283286

284287
#if ENABLE_NGROK_COMMUNITY_PLUGIN
285288
public static void ConfigureNgrok(this IDistributedApplicationBuilder builder,

AppHost/README.md

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ the services wait for the database and secrets setup to finish before launching.
3030
| `setup-secrets` | Executable | Runs `dev/setup_secrets.ps1` — applies `dev/secrets.json` to all projects |
3131
| `mssql` | SQL Server 2022 container | Persistent data volume, port 1433 |
3232
| `run-db-migrations` | Executable | Runs `dev/migrate.ps1` against `vault_dev` (or `self_host_dev`) |
33-
| `azurite` | Azure Storage emulator | Blob :10000 · Queue :10001 · Table :10002 |
33+
| `azurite` | Azure Storage emulator | Blob :10000 · Queue :10001 · Table :10002 · persistent data volume |
3434
| `azurite-setup` | Executable | Runs `dev/setup_azurite.ps1` after Azurite is ready |
3535
| `mailcatcher` | Container | SMTP :10250 · Web UI :1080 |
3636
| `redis` | Container | Redis with AOF persistence, port 6379 |
@@ -71,7 +71,7 @@ dotnet user-secrets set "Database:Password" "<your-sa-password>"
7171
| Key | Default | Description |
7272
|-----------------------------|------------------------------------|--------------------------------------------------------------------------------------|
7373
| `SelfHost` | `false` | Switch to self-hosted mode (see [Self-Hosted Mode](#self-hosted-mode)) |
74-
| `ClientsPath` | `../../clients/apps` | Path to the `clients` repo's `apps/` directory (used by the Node.js plugin) |
74+
| `ClientsPath` | `../../clients/apps` | Path to the `clients` repo's `apps/` directory (see [Git Worktrees](#git-worktrees)) |
7575
| `WorkingDirectory` | `../dev` | Directory where dev scripts are resolved |
7676
| `Services:<name>:BasePort` | see `appsettings.Development.json` | HTTP port for each service; pre-filled to match each service's `launchSettings.json` |
7777
| `Database:Image` | `mssql/server:2022-latest` | Docker image for SQL Server |
@@ -85,33 +85,23 @@ dotnet user-secrets set "Database:Password" "<your-sa-password>"
8585
| `MailCatcher:SmtpPort` | `10250` | Host SMTP port |
8686
| `MailCatcher:WebPort` | `1080` | MailCatcher web UI port |
8787
| `NgrokAuthToken` | _(empty)_ | ngrok auth token (used only when ngrok plugin is enabled) |
88-
| `WebFrontend:Port` | `8080` | Web frontend port (used only when Node.js plugin is enabled) |
88+
| `WebFrontend:Port` | `8080` | Web frontend port |
8989
| `WebFrontend:Url` | `https://bitwarden.test:8080` | Web frontend URL shown in the dashboard |
9090

9191
## Optional Features
9292

93-
### Web Frontend (Node.js community plugin)
93+
### Web Frontend
9494

9595
Runs the web client alongside the server services. Requires the Bitwarden
9696
[clients](https://github.com/bitwarden/clients) repo cloned as a sibling to `server`.
9797

98-
1. Create an `AppHost.csproj.user` file next to `AppHost.csproj` (it is covered by `.gitignore`):
99-
100-
```xml
101-
<Project>
102-
<PropertyGroup>
103-
<EnableNodeJsCommunityPlugin>true</EnableNodeJsCommunityPlugin>
104-
</PropertyGroup>
105-
</Project>
106-
```
107-
108-
2. If the clients repo is not at `../../clients/apps`, override the path:
98+
1. If the clients repo is not at `../../clients/apps`, override the path:
10999

110100
```bash
111101
dotnet user-secrets set "ClientsPath" "<path/to/clients/apps>"
112102
```
113103

114-
3. Run `dotnet run` as normal. The `web-frontend` resource starts with **explicit start** — open
104+
2. Run `dotnet run` as normal. The `web-frontend` resource starts with **explicit start** — open
115105
the Aspire dashboard and start it manually when you're ready.
116106

117107
### Ngrok (Billing Webhook Tunneling)
@@ -194,6 +184,26 @@ variables for every resource.
194184
- If you create an `appsettings.local.json`, add it to `.gitignore` before writing any values
195185
to it.
196186

187+
## Git Worktrees
188+
189+
Path-based settings resolve relative to where you run `dotnet run`. If your worktree lives in
190+
a different location than the main checkout, those paths won't resolve correctly. Use absolute
191+
paths for any such setting:
192+
193+
- **`ClientsPath`** defaults to `../../clients/apps` — override if your worktree is not
194+
alongside the `clients` repo:
195+
196+
```bash
197+
dotnet user-secrets set "ClientsPath" "<absolute/path/to/clients/apps>"
198+
```
199+
200+
- **`AdditionalProjects:<name>:Path`** — use an absolute path when adding projects via user
201+
secrets in a worktree:
202+
203+
```bash
204+
dotnet user-secrets set "AdditionalProjects:<name>:Path" "<absolute/path/to/Project.csproj>"
205+
```
206+
197207
## Troubleshooting
198208

199209
| Symptom | Fix |
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using Bit.Api.AdminConsole.Authorization;
2+
using Bit.Core.AdminConsole.Entities;
3+
using Bit.Core.Exceptions;
4+
using Bit.Core.Repositories;
5+
using Microsoft.AspNetCore.Mvc;
6+
using Microsoft.AspNetCore.Mvc.ModelBinding;
7+
8+
namespace Bit.Api.AdminConsole.Attributes;
9+
10+
/// <summary>
11+
/// Binds an <see cref="Organization"/> parameter by loading it from the database
12+
/// using the <c>orgId</c> or <c>organizationId</c> route parameter.
13+
/// </summary>
14+
/// <remarks>
15+
/// If the organization is not found, a <see cref="NotFoundException"/> is thrown.
16+
/// </remarks>
17+
/// <example>
18+
/// <code><![CDATA[
19+
/// [HttpPost("bulk-auto-confirm")]
20+
/// public async Task<IResult> BulkAutomaticallyConfirmOrganizationUsersAsync(
21+
/// [BindOrganization] Organization organization,
22+
/// [FromBody] OrganizationUserBulkConfirmRequestModel model)
23+
/// ]]></code>
24+
/// </example>
25+
[AttributeUsage(AttributeTargets.Parameter)]
26+
public sealed class BindOrganizationAttribute() : ModelBinderAttribute(typeof(OrganizationModelBinder));
27+
28+
/// <summary>
29+
/// Custom model binder that loads an <see cref="Organization"/> from the database
30+
/// using the <c>orgId</c> or <c>organizationId</c> route parameter and binds it to the parameter.
31+
/// </summary>
32+
/// <remarks>
33+
/// This binder is used via the <see cref="BindOrganizationAttribute"/>.
34+
/// </remarks>
35+
public class OrganizationModelBinder : IModelBinder
36+
{
37+
public async Task BindModelAsync(ModelBindingContext bindingContext)
38+
{
39+
Guid orgId;
40+
try
41+
{
42+
orgId = bindingContext.HttpContext.GetOrganizationId();
43+
}
44+
catch (InvalidOperationException)
45+
{
46+
throw new BadRequestException("Route parameter 'orgId' or 'organizationId' is missing or invalid.");
47+
}
48+
49+
var repo = bindingContext.HttpContext.RequestServices
50+
.GetRequiredService<IOrganizationRepository>();
51+
52+
var organization = await repo.GetByIdAsync(orgId);
53+
if (organization is null)
54+
{
55+
throw new NotFoundException();
56+
}
57+
58+
bindingContext.Result = ModelBindingResult.Success(organization);
59+
}
60+
}

src/Api/AdminConsole/Controllers/OrganizationInviteLinksController.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ public class OrganizationInviteLinksController(
1919
IGetOrganizationInviteLinkStatusQuery getOrganizationInviteLinkStatusQuery,
2020
IUpdateOrganizationInviteLinkCommand updateOrganizationInviteLinkCommand,
2121
IDeleteOrganizationInviteLinkCommand deleteOrganizationInviteLinkCommand,
22-
IRefreshOrganizationInviteLinkCommand refreshOrganizationInviteLinkCommand)
22+
IRefreshOrganizationInviteLinkCommand refreshOrganizationInviteLinkCommand,
23+
IValidateOrganizationInviteLinkEmailDomainQuery validateOrganizationInviteLinkEmailDomainQuery)
2324
: BaseAdminConsoleController
2425
{
2526
[AllowAnonymous]
@@ -37,6 +38,17 @@ status.Sso is null
3738
: new OrganizationInviteLinkSsoResponseModel(status.Sso.OrgSsoId, status.Sso.Required))));
3839
}
3940

41+
[AllowAnonymous]
42+
[HttpPost("/organizations/invite-link/validate-email-domain")]
43+
public async Task<IResult> ValidateEmailDomain(
44+
[FromBody] OrganizationInviteLinkValidateEmailDomainRequestModel model)
45+
{
46+
var result = await validateOrganizationInviteLinkEmailDomainQuery.ValidateAsync(model.Code, model.Email);
47+
48+
return Handle(result, isAllowed =>
49+
TypedResults.Ok(new OrganizationInviteLinkValidateEmailDomainResponseModel(isAllowed)));
50+
}
51+
4052
[HttpGet("")]
4153
[Authorize<ManageUsersRequirement>]
4254
public async Task<IResult> Get(Guid orgId)

src/Api/AdminConsole/Controllers/OrganizationUsersController.cs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Bit.Api.AdminConsole.Models.Response.Organizations;
1111
using Bit.Api.Models.Response;
1212
using Bit.Core;
13+
using Bit.Core.AdminConsole.Entities;
1314
using Bit.Core.AdminConsole.Models.Data;
1415
using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
1516
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
@@ -42,6 +43,7 @@
4243
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
4344
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
4445
using Microsoft.AspNetCore.Authorization;
46+
using Microsoft.AspNetCore.Identity;
4547
using Microsoft.AspNetCore.Mvc;
4648
using AccountRecoveryV2 = Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery.v2;
4749
using V1_RevokeOrganizationUserCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1.IRevokeOrganizationUserCommand;
@@ -229,10 +231,11 @@ private ListResponseModel<OrganizationUserUserDetailsResponseModel> GetResultLis
229231

230232
[HttpGet("{id}/reset-password-details")]
231233
[Authorize<ManageAccountRecoveryRequirement>]
232-
public async Task<OrganizationUserResetPasswordDetailsResponseModel> GetResetPasswordDetails(Guid orgId, Guid id)
234+
public async Task<OrganizationUserResetPasswordDetailsResponseModel> GetResetPasswordDetails(Guid id,
235+
[BindOrganization] Organization organization)
233236
{
234237
var organizationUser = await _organizationUserRepository.GetByIdAsync(id);
235-
if (organizationUser is null || organizationUser.OrganizationId != orgId || organizationUser.UserId is null)
238+
if (organizationUser is null || organizationUser.OrganizationId != organization.Id || organizationUser.UserId is null)
236239
{
237240
throw new NotFoundException();
238241
}
@@ -245,14 +248,7 @@ public async Task<OrganizationUserResetPasswordDetailsResponseModel> GetResetPas
245248
throw new NotFoundException();
246249
}
247250

248-
// Retrieve Encrypted Private Key from organization
249-
var org = await _organizationRepository.GetByIdAsync(orgId);
250-
if (org == null)
251-
{
252-
throw new NotFoundException();
253-
}
254-
255-
return new OrganizationUserResetPasswordDetailsResponseModel(new OrganizationUserResetPasswordDetails(organizationUser, user, org));
251+
return new OrganizationUserResetPasswordDetailsResponseModel(new OrganizationUserResetPasswordDetails(organizationUser, user, organization));
256252
}
257253

258254
[HttpPost("account-recovery-details")]
@@ -534,8 +530,17 @@ public async Task<IResult> PutRecoverAccount(Guid orgId, Guid id, [FromBody] Org
534530
return Handle(await _adminRecoverAccountCommandV2.RecoverAccountAsync(commandRequest));
535531
}
536532

537-
var result = await _adminRecoverAccountCommand.RecoverAccountAsync(
538-
orgId, targetOrganizationUser, model.NewMasterPasswordHash!, model.Key!);
533+
IdentityResult result;
534+
if (model.RequestHasNewDataTypes())
535+
{
536+
result = await _adminRecoverAccountCommand.RecoverAccountAsync(
537+
orgId, targetOrganizationUser, model.UnlockData!.ToData(), model.AuthenticationData!.ToData());
538+
}
539+
else
540+
{
541+
result = await _adminRecoverAccountCommand.RecoverAccountAsync(
542+
orgId, targetOrganizationUser, model.NewMasterPasswordHash!, model.Key!);
543+
}
539544
if (result.Succeeded)
540545
{
541546
return TypedResults.Ok();
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace Bit.Api.AdminConsole.Models.Request.Organizations;
4+
5+
public class OrganizationInviteLinkValidateEmailDomainRequestModel
6+
{
7+
[Required]
8+
public required Guid Code { get; set; }
9+
10+
[Required]
11+
[EmailAddress]
12+
public required string Email { get; set; }
13+
}

0 commit comments

Comments
 (0)