Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
bd50261
[PM-24279] Add endpoint
JimmyVo16 Aug 26, 2025
4dd1a48
[PM-24279] code review
JimmyVo16 Aug 27, 2025
7e75497
[PM-24279] fix tests
JimmyVo16 Aug 27, 2025
ff12cb6
Merge branch 'main' into ac/pm-24279/add-vnext-policy-endpoint
JimmyVo16 Aug 28, 2025
42d8213
[PM-24278] Add IPostSavePolicySideEffect
JimmyVo16 Aug 28, 2025
c636638
[PM-24278] use IPostSavePolicySideEffect
JimmyVo16 Aug 28, 2025
5e9b957
[PM-24278] DI
JimmyVo16 Aug 28, 2025
a57f268
Merge branch 'ac/pm-24278/add-ipostsavepolicysideeffect' into ac/pm-2โ€ฆ
JimmyVo16 Aug 28, 2025
83d9779
[PM-24279] wip
JimmyVo16 Aug 28, 2025
123808b
[PM-24279] wip
JimmyVo16 Aug 29, 2025
7da4718
[PM-24279] Fix tests
JimmyVo16 Aug 29, 2025
4f5e545
[PM-24279] wip
JimmyVo16 Aug 29, 2025
09bb0df
Merge branch 'main' into ac/pm-24279/add-vnext-policy-endpoint
JimmyVo16 Aug 29, 2025
0b82347
Merge branch 'main' into ac/pm-24279/add-vnext-policy-endpoint
JimmyVo16 Sep 2, 2025
f8ddacd
[PM-24279] Add tests
JimmyVo16 Sep 2, 2025
6c3c60e
Merge branch 'main' into ac/pm-24279/add-vnext-policy-endpoint
JimmyVo16 Sep 2, 2025
b37ec5b
Merge branch 'main' into ac/pm-24279/add-vnext-policy-endpoint
JimmyVo16 Sep 3, 2025
5497657
Merge branch 'main' into ac/pm-24279/add-vnext-policy-endpoint
JimmyVo16 Sep 3, 2025
7f78c1c
Merge branch 'main' into ac/pm-24279/add-vnext-policy-endpoint
JimmyVo16 Sep 4, 2025
fa69074
[PM-24279] Add API integration tests
JimmyVo16 Sep 5, 2025
e93f2a2
Merge branch 'main' into ac/pm-24279/add-vnext-policy-endpoint
JimmyVo16 Sep 5, 2025
268d74f
Merge branch 'main' into ac/pm-24279/add-vnext-policy-endpoint
JimmyVo16 Sep 9, 2025
914ef0b
Update src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValiโ€ฆ
JimmyVo16 Sep 9, 2025
46b9bdf
Merge branch 'ac/pm-24279/add-vnext-policy-endpoint' of https://githuโ€ฆ
JimmyVo16 Sep 9, 2025
95c3f8f
[PM-24279] code review
JimmyVo16 Sep 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/Api/AdminConsole/Controllers/PoliciesController.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
๏ปฟ// FIXME: Update this file to be null safe and then delete the line below
#nullable disable

using Bit.Api.AdminConsole.Authorization;
using Bit.Api.AdminConsole.Authorization.Requirements;
using Bit.Api.AdminConsole.Models.Request;
using Bit.Api.AdminConsole.Models.Response.Helpers;
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
Expand Down Expand Up @@ -212,4 +215,16 @@ public async Task<PolicyResponseModel> Put(Guid orgId, PolicyType type, [FromBod
var policy = await _savePolicyCommand.SaveAsync(policyUpdate);
return new PolicyResponseModel(policy);
}


[HttpPut("{type}/vnext")]
[RequireFeatureAttribute(FeatureFlagKeys.CreateDefaultLocation)]
[Authorize<ManagePoliciesRequirement>]
public async Task<PolicyResponseModel> PutVNext(Guid orgId, PolicyType type, [FromBody] SavePolicyRequest model)
{
// This logic will be fleshed out in the following PRs in PM-24279
var savePolicyModel = await model.ToSavePolicyModelAsync(orgId, _currentContext);
return new PolicyResponseModel(new Policy());
}

}
61 changes: 61 additions & 0 deletions src/Api/AdminConsole/Models/Request/SavePolicyRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
๏ปฟusing System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.Context;
using Bit.Core.Utilities;

namespace Bit.Api.AdminConsole.Models.Request;

public class SavePolicyRequest
{
[Required]
public PolicyRequestModel Policy { get; set; } = null!;

public Dictionary<string, object>? Metadata { get; set; }

public async Task<SavePolicyModel> ToSavePolicyModelAsync(Guid organizationId, ICurrentContext currentContext)
{
var performedBy = new StandardUser(currentContext.UserId!.Value, await currentContext.OrganizationOwner(organizationId));

var updatedPolicy = new PolicyUpdate()
{
Type = Policy.Type!.Value,
OrganizationId = organizationId,
Data = Policy.Data != null ? JsonSerializer.Serialize(Policy.Data) : null,
Enabled = Policy.Enabled.GetValueOrDefault(),
};

var metadata = MapToPolicyMetadata();

return new SavePolicyModel(updatedPolicy, performedBy, metadata);
}

private IPolicyMetadataModel MapToPolicyMetadata()
{
if (Metadata == null)
{
return new EmptyMetadataModel();
}

return Policy?.Type switch
{
PolicyType.OrganizationDataOwnership => MapToPolicyMetadata<OrganizationModelOwnershipPolicyModel>(),
_ => new EmptyMetadataModel()
};
Comment on lines +42 to +46
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(non-blocking) I want us to think about how this scales. It's not a huge deal right now because this is the only policy that uses this model. But as policies are added that use this pattern, this is an unexpected place that teams have to know to come and update, and it has some overhead in figuring out how it's passed through to their method.

There is also a runtime issue (I think), where your side effect method is going to cast this to get the underlying type (rather than IPolicyMetadataModel), but that assumes that it's been correctly instantiated as the correct type by the api layer. This creates an unexpected dependency.

There is an advantage to the Policy.Data pattern, where the model is passed through as a json blob and only deserialized by the code that needs itย when it needs it (in this case, the side effect method).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is an unexpected place that teams have to know to come and update

Youโ€™re right about this.

There is an advantage to the Policy.Data pattern, where the model is passed through as a json blob and only deserialized by the code that needs it when it needs it (in this case, the side effect method).

So we can follow this pattern, but weโ€™ll run into this problem: currently, it doesnโ€™t look like weโ€™re validating Policy.Data at the controller level. If it fails, it seems like it will fail silently or return a 500 at best.

Ideally, if the client passes in invalid data, we should return a 422 with a helpful message. Iโ€™m guessing validation problems never arise because the fields in Policy.Data are optional?

For this PR

I think the trade-offs are either:

  1. Use the existing pattern like Policy.Data and not have validation, which isnโ€™t great since we have a required field.
  2. Have the controller handle it and return a helpful error message, but this will have to live in the application layer, so it will be another place developers just have to know.

I'm open to any other ideas

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have validation on Policy.Data as well, so that is a bit of an unsolved problem. Ideally the json deserialization library would handle this, but I haven't looked into it. I would err on the side of following existing patterns, option 1, then we can return to the validation problem later.

That said, I understand we're trying to get this shipped, so I have no hard requirement here; whatever's reasonable.

}

private IPolicyMetadataModel MapToPolicyMetadata<T>() where T : IPolicyMetadataModel, new()
{
try
{
var json = JsonSerializer.Serialize(Metadata);
return CoreHelpers.LoadClassFromJsonData<T>(json);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eliykat Currently, if the JSON format doesnโ€™t match the C# DTO, it just returns an empty object of that C# DTO. Is this acceptable behavior, or should we handle it differently?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's OK generally, except that in this case the collection name is required. So we still want some kind of check & throw if the collection name is not provided.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The outcome of this discussion will dictate this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the other thread, we can return to validation later.

}
catch
{
return new EmptyMetadataModel();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
๏ปฟ
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;

public record EmptyMetadataModel : IPolicyMetadataModel
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
๏ปฟ
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;

public interface IPolicyMetadataModel
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
๏ปฟ
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;

public class OrganizationModelOwnershipPolicyModel : IPolicyMetadataModel
{
public OrganizationModelOwnershipPolicyModel()
{
}

public OrganizationModelOwnershipPolicyModel(string? defaultUserCollectionName)
{
DefaultUserCollectionName = defaultUserCollectionName;
}

public string? DefaultUserCollectionName { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
๏ปฟ
using Bit.Core.AdminConsole.Models.Data;

namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;

public record SavePolicyModel(PolicyUpdate PolicyUpdate, IActingUser? PerformedBy, IPolicyMetadataModel Metadata)
{
public PolicyUpdate PolicyUpdate { get; init; } = PolicyUpdate;
public IPolicyMetadataModel Metadata { get; init; } = Metadata;

public IActingUser? PerformedBy { get; init; } = PerformedBy;
}
Loading
Loading