Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-14439] Add PolicyRequirements for enforcement logic #5336

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
๏ปฟusing Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.Enums;
using Bit.Core.Utilities;

namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;

public class OrganizationUserPolicyDetails
{
public Guid OrganizationUserId { get; set; }
public Guid? OrganizationUserId { get; set; }

public Guid OrganizationId { get; set; }

Expand All @@ -22,4 +24,9 @@
public string OrganizationUserPermissionsData { get; set; }

public bool IsProvider { get; set; }

public T GetDataModel<T>() where T : IPolicyDataModel, new()
{
return CoreHelpers.LoadClassFromJsonData<T>(PolicyData);
}

Check warning on line 31 in src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs#L29-L31

Added lines #L29 - L31 were not covered by tests
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
๏ปฟusing Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Billing.Enums;
Expand All @@ -13,7 +15,7 @@
using Bit.Core.Utilities;
using Microsoft.AspNetCore.DataProtection;

namespace Bit.Core.OrganizationFeatures.OrganizationUsers;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;

public class AcceptOrgUserCommand : IAcceptOrgUserCommand
{
Expand All @@ -25,6 +27,8 @@
private readonly IMailService _mailService;
private readonly IUserRepository _userRepository;
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
private readonly IFeatureService _featureService;

public AcceptOrgUserCommand(
IDataProtectionProvider dataProtectionProvider,
Expand All @@ -34,7 +38,9 @@
IPolicyService policyService,
IMailService mailService,
IUserRepository userRepository,
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory)
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
IPolicyRequirementQuery policyRequirementQuery,
IFeatureService featureService)
{

// TODO: remove data protector when old token validation removed
Expand All @@ -46,8 +52,13 @@
_mailService = mailService;
_userRepository = userRepository;
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
_policyRequirementQuery = policyRequirementQuery;
_featureService = featureService;
}

public const string ErrorBlockedByThisSingleOrganizationPolicy = "You may not join this organization until you leave or remove all other organizations.";
public const string ErrorBlockedByOtherSingleOrganizationPolicy = "You cannot join this organization because you are a member of another organization which forbids it";

public async Task<OrganizationUser> AcceptOrgUserByEmailTokenAsync(Guid organizationUserId, User user, string emailToken,
IUserService userService)
{
Expand Down Expand Up @@ -172,7 +183,35 @@
}
}

// Enforce Single Organization Policy of organization user is trying to join
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
{
await EnforcePolicies_vNext(user, orgUser, userService);
}

Check warning on line 189 in src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs#L189

Added line #L189 was not covered by tests
else
{
await EnforcePolicies(user, orgUser, userService);
}

orgUser.Status = OrganizationUserStatusType.Accepted;
orgUser.UserId = user.Id;
orgUser.Email = null;

await _organizationUserRepository.ReplaceAsync(orgUser);

var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync(orgUser.OrganizationId, OrganizationUserType.Admin);
var adminEmails = admins.Select(a => a.Email).Distinct().ToList();

if (adminEmails.Count > 0)
{
var organization = await _organizationRepository.GetByIdAsync(orgUser.OrganizationId);
await _mailService.SendOrganizationAcceptedEmailAsync(organization, user.Email, adminEmails);
}

return orgUser;
}

private async Task EnforcePolicies(User user, OrganizationUser orgUser, IUserService userService)
{
var allOrgUsers = await _organizationUserRepository.GetManyByUserAsync(user.Id);
var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId);
var invitedSingleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
Expand Down Expand Up @@ -201,23 +240,34 @@
throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account.");
}
}
}

orgUser.Status = OrganizationUserStatusType.Accepted;
orgUser.UserId = user.Id;
orgUser.Email = null;

await _organizationUserRepository.ReplaceAsync(orgUser);

var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync(orgUser.OrganizationId, OrganizationUserType.Admin);
var adminEmails = admins.Select(a => a.Email).Distinct().ToList();
private async Task EnforcePolicies_vNext(User user, OrganizationUser orgUser, IUserService userService)
{
var singleOrganizationRequirement =
await _policyRequirementQuery.GetAsync<SingleOrganizationPolicyRequirement>(user.Id);

if (adminEmails.Count > 0)
switch (singleOrganizationRequirement.CanJoinOrganization(orgUser.OrganizationId))
{
var organization = await _organizationRepository.GetByIdAsync(orgUser.OrganizationId);
await _mailService.SendOrganizationAcceptedEmailAsync(organization, user.Email, adminEmails);
case SingleOrganizationRequirementResult.BlockedByThisOrganization:
throw new BadRequestException(ErrorBlockedByThisSingleOrganizationPolicy);
case SingleOrganizationRequirementResult.BlockedByOtherOrganization:
throw new BadRequestException(ErrorBlockedByOtherSingleOrganizationPolicy);
case SingleOrganizationRequirementResult.Ok:
default:
break;
}

return orgUser;
// TODO: this will be rewritten once a requirement is added
// Enforce Two Factor Authentication Policy of organization user is trying to join
if (!await userService.TwoFactorIsEnabledAsync(user))
{
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
{
throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account.");
}
}

Check warning on line 271 in src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs#L271

Added line #L271 was not covered by tests
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
๏ปฟusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;

namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;

public interface IPolicyRequirementQuery
{
Task<T> GetAsync<T>(Guid userId) where T : IPolicyRequirement;
}
Copy link
Member Author

Choose a reason for hiding this comment

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

There is an intentional split of responsibilities here: all business logic is in the policy requirements, which are written in a functional style. The query is agnostic about what policies are being handled - its only job is to connect policy requirements to dependencies.

Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
๏ปฟusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Settings;

Check warning on line 4 in src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs

View workflow job for this annotation

GitHub Actions / Lint

Using directive is unnecessary.

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

public class PolicyRequirementQuery : IPolicyRequirementQuery
{
private readonly IPolicyRepository _policyRepository;
private readonly PolicyRequirementRegistry _policyRequirements = new();

public PolicyRequirementQuery(IPolicyRepository policyRepository)
{
_policyRepository = policyRepository;

// Register Policy Requirement factory functions below
_policyRequirements.Add(SingleOrganizationPolicyRequirement.Create);
}

public async Task<T> GetAsync<T>(Guid userId) where T : IPolicyRequirement
=> _policyRequirements.Get<T>()(await GetPolicyDetails(userId));

Check warning on line 22 in src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs#L22

Added line #L22 was not covered by tests

private Task<IEnumerable<OrganizationUserPolicyDetails>> GetPolicyDetails(Guid userId) =>
_policyRepository.GetPolicyDetailsByUserId(userId);

Check warning on line 25 in src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs#L25

Added line #L25 was not covered by tests

/// <summary>
/// Helper class used to register and retrieve Policy Requirement factories by type.
/// </summary>
private class PolicyRequirementRegistry
{
private readonly Dictionary<Type, CreateRequirement<IPolicyRequirement>> _registry = new();

public void Add<T>(CreateRequirement<T> factory) where T : IPolicyRequirement
{
// Explicitly convert T to an IPolicyRequirement (C# doesn't do this automatically).
IPolicyRequirement Converted(IEnumerable<OrganizationUserPolicyDetails> up) => factory(up);

Check warning on line 37 in src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs#L37

Added line #L37 was not covered by tests
_registry.Add(typeof(T), Converted);
}

public CreateRequirement<T> Get<T>() where T : IPolicyRequirement
{

Check warning on line 42 in src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs#L42

Added line #L42 was not covered by tests
if (!_registry.TryGetValue(typeof(T), out var factory))
{

Check warning on line 44 in src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs#L44

Added line #L44 was not covered by tests
throw new NotImplementedException("No Policy Requirement found for " + typeof(T));
}

// Explicitly convert IPolicyRequirement back to T (C# doesn't do this automatically).
// The cast here relies on the Register method correctly associating the type and factory function.
T Converted(IEnumerable<OrganizationUserPolicyDetails> up) => (T)factory(up);
return Converted;
}

Check warning on line 52 in src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs#L50-L52

Added lines #L50 - L52 were not covered by tests
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
๏ปฟusing Bit.Core.Models.Data.Organizations.OrganizationUsers;

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

public interface IPolicyRequirement;

public delegate T CreateRequirement<T>(IEnumerable<OrganizationUserPolicyDetails> userPolicyDetails)
where T : IPolicyRequirement;
Copy link
Contributor

Choose a reason for hiding this comment

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

This is great!

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
๏ปฟusing Bit.Core.AdminConsole.Enums;
using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;

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

public static class PolicyRequirementHelpers
{
public static IEnumerable<OrganizationUserPolicyDetails> GetPolicyType(
this IEnumerable<OrganizationUserPolicyDetails> userPolicyDetails,
PolicyType type) =>
userPolicyDetails.Where(x => x.PolicyType == type);

public static IEnumerable<OrganizationUserPolicyDetails> ExcludeOwnersAndAdmins(
this IEnumerable<OrganizationUserPolicyDetails> userPolicyDetails) =>
userPolicyDetails.Where(x => x.OrganizationUserType != OrganizationUserType.Owner);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
userPolicyDetails.Where(x => x.OrganizationUserType != OrganizationUserType.Owner);
userPolicyDetails.Where(x => x.OrganizationUserType is not OrganizationUserType.Owner and not OrganizationUserType.Admin);


public static IEnumerable<OrganizationUserPolicyDetails> ExcludeProviders(
this IEnumerable<OrganizationUserPolicyDetails> userPolicyDetails) =>
userPolicyDetails.Where(x => !x.IsProvider);

public static IEnumerable<OrganizationUserPolicyDetails> ExcludeRevokedAndInvitedUsers(
this IEnumerable<OrganizationUserPolicyDetails> userPolicyDetails) =>
userPolicyDetails.Where(x => x.OrganizationUserStatus >= OrganizationUserStatusType.Accepted);

public static IEnumerable<OrganizationUserPolicyDetails> ExcludeRevokedUsers(
this IEnumerable<OrganizationUserPolicyDetails> userPolicyDetails) =>
userPolicyDetails.Where(x => x.OrganizationUserStatus >= OrganizationUserStatusType.Invited);

Check warning on line 28 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyRequirementHelpers.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyRequirementHelpers.cs#L28

Added line #L28 was not covered by tests
Copy link
Contributor

Choose a reason for hiding this comment

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

Not a huge fan of using > and < on enum values. Maybe its better to add an explicit list of allowed types for each and just do a contains?

๐Ÿคท non-blocking. just a thought.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
๏ปฟusing Bit.Core.AdminConsole.Enums;
using Bit.Core.Enums;

Check warning on line 2 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs

View workflow job for this annotation

GitHub Actions / Lint

Using directive is unnecessary.
using Bit.Core.Models.Data.Organizations.OrganizationUsers;

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

public enum SingleOrganizationRequirementResult
{
Ok = 1,
BlockedByThisOrganization = 2,
BlockedByOtherOrganization = 3
}

public class SingleOrganizationPolicyRequirement : IPolicyRequirement
{
/// <summary>
/// Single organization policy details filtered by user role but not status.
/// This lets us check whether a user is compliant before being accepted/restored.
/// </summary>
private IEnumerable<OrganizationUserPolicyDetails> PolicyDetails { get; init; }

public static SingleOrganizationPolicyRequirement Create(IEnumerable<OrganizationUserPolicyDetails> userPolicyDetails)
=> new()
{
PolicyDetails = userPolicyDetails
.GetPolicyType(PolicyType.SingleOrg)
.ExcludeOwnersAndAdmins()
.ExcludeProviders()
.ToList()
};

public SingleOrganizationRequirementResult CanJoinOrganization(Guid organizationId)
{
// Check for the org the user is trying to join; status doesn't matter
if (PolicyDetails.Any(x => x.OrganizationId == organizationId))
{
return SingleOrganizationRequirementResult.BlockedByThisOrganization;
}

// Check for other orgs the user might already be a member of (accepted or confirmed status only)
if (PolicyDetails.ExcludeRevokedAndInvitedUsers().Any())
{
return SingleOrganizationRequirementResult.BlockedByOtherOrganization;
}

return SingleOrganizationRequirementResult.Ok;
}

public SingleOrganizationRequirementResult CanBeRestoredToOrganization(Guid organizationId) =>
CanJoinOrganization(organizationId);

Check warning on line 50 in src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs#L50

Added line #L50 was not covered by tests
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public static void AddPolicyServices(this IServiceCollection services)
{
services.AddScoped<IPolicyService, PolicyService>();
services.AddScoped<ISavePolicyCommand, SavePolicyCommand>();
services.AddScoped<IPolicyRequirementQuery, PolicyRequirementQuery>();

services.AddScoped<IPolicyValidator, TwoFactorAuthenticationPolicyValidator>();
services.AddScoped<IPolicyValidator, SingleOrgPolicyValidator>();
Expand Down
2 changes: 2 additions & 0 deletions src/Core/AdminConsole/Repositories/IPolicyRepository.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
๏ปฟusing Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;

#nullable enable
Expand All @@ -11,4 +12,5 @@ public interface IPolicyRepository : IRepository<Policy, Guid>
Task<Policy?> GetByOrganizationIdTypeAsync(Guid organizationId, PolicyType type);
Task<ICollection<Policy>> GetManyByOrganizationIdAsync(Guid organizationId);
Task<ICollection<Policy>> GetManyByUserIdAsync(Guid userId);
Task<IEnumerable<OrganizationUserPolicyDetails>> GetPolicyDetailsByUserId(Guid userId);
}
1 change: 1 addition & 0 deletions src/Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ public static class FeatureFlagKeys
public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications";
public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission";
public const string PushSyncOrgKeysOnRevokeRestore = "pm-17168-push-sync-org-keys-on-revoke-restore";
public const string PolicyRequirements = "pm-14439-policy-requirements";

/* Tools Team */
public const string ItemShare = "item-share";
Expand Down
2 changes: 1 addition & 1 deletion src/Core/Services/Implementations/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1372,7 +1372,7 @@ private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user)
await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
new RevokeOrganizationUsersRequest(
p.OrganizationId,
[new OrganizationUserUserDetails { Id = p.OrganizationUserId, OrganizationId = p.OrganizationId }],
[new OrganizationUserUserDetails { Id = p.OrganizationUserId!.Value, OrganizationId = p.OrganizationId }],
new SystemUser(EventSystemUser.TwoFactorDisabled)));
await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Settings;
using Bit.Infrastructure.Dapper.Repositories;
using Dapper;
Expand Down Expand Up @@ -59,4 +60,17 @@
return results.ToList();
}
}

public async Task<IEnumerable<OrganizationUserPolicyDetails>> GetPolicyDetailsByUserId(Guid userId)
{
using (var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<OrganizationUserPolicyDetails>(
$"[{Schema}].[PolicyDetails_ReadByUserId]",
new { UserId = userId },
commandType: CommandType.StoredProcedure);

Check warning on line 71 in src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs

View check run for this annotation

Codecov / codecov/patch

src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs#L65-L71

Added lines #L65 - L71 were not covered by tests

return results.ToList();

Check warning on line 73 in src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs

View check run for this annotation

Codecov / codecov/patch

src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs#L73

Added line #L73 was not covered by tests
}
}

Check warning on line 75 in src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs

View check run for this annotation

Codecov / codecov/patch

src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs#L75

Added line #L75 was not covered by tests
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
๏ปฟusing AutoMapper;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries;
using Bit.Infrastructure.EntityFramework.Repositories;
Expand Down Expand Up @@ -50,4 +51,6 @@
return Mapper.Map<List<AdminConsoleEntities.Policy>>(results);
}
}

public Task<IEnumerable<OrganizationUserPolicyDetails>> GetPolicyDetailsByUserId(Guid userId) => throw new NotImplementedException();

Check warning on line 55 in src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs

View check run for this annotation

Codecov / codecov/patch

src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs#L55

Added line #L55 was not covered by tests
}
Loading
Loading