1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-01 16:12:49 -05:00

Implemented Custom role and permissions (#1057)

* Implemented Custom role and permissions

* Converted permissions columns to a json blob

* Code review fixes for Permissions

* sql build fix

* Update Permissions.cs

* formatting

* Update IOrganizationService.cs

* reworked a conditional

* built out tests for relevant organization service methods

* removed unused usings

* fixed a broken test and a bad empty string init

* removed 'Attribute' from some attribute instances
This commit is contained in:
Addison Beck
2021-01-12 11:02:39 -05:00
committed by GitHub
parent 99b95b5330
commit 63fcdc1418
39 changed files with 1116 additions and 149 deletions

View File

@ -8,6 +8,7 @@ using Bit.Core.Repositories;
using System.Threading.Tasks;
using System.Security.Claims;
using Bit.Core.Utilities;
using Bit.Core.Models.Data;
namespace Bit.Core
{
@ -148,6 +149,18 @@ namespace Bit.Core
Type = OrganizationUserType.Manager
}));
}
if (claimsDict.ContainsKey("orgcustom"))
{
Organizations.AddRange(claimsDict["orgcustom"].Select(c =>
new CurrentContentOrganization
{
Id = new Guid(c.Value),
Type = OrganizationUserType.Custom,
Permissions = SetOrganizationPermissionsFromClaims(c.Value, claimsDict)
}));
}
return Task.FromResult(0);
}
@ -174,6 +187,61 @@ namespace Bit.Core
return Organizations?.Any(o => o.Id == orgId && o.Type == OrganizationUserType.Owner) ?? false;
}
public bool OrganizationCustom(Guid orgId)
{
return Organizations?.Any(o => o.Id == orgId && o.Type == OrganizationUserType.Custom) ?? false;
}
public bool AccessBusinessPortal(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId && o.Permissions.AccessBusinessPortal) ?? false);
}
public bool AccessEventLogs(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId && o.Permissions.AccessEventLogs) ?? false);
}
public bool AccessImportExport(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId && o.Permissions.AccessImportExport) ?? false);
}
public bool AccessReports(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId && o.Permissions.AccessReports) ?? false);
}
public bool ManageAllCollections(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId && o.Permissions.ManageAllCollections) ?? false);
}
public bool ManageAssignedCollections(Guid orgId)
{
return OrganizationManager(orgId) || (Organizations?.Any(o => o.Id == orgId && o.Permissions.ManageAssignedCollections) ?? false);
}
public bool ManageGroups(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId && o.Permissions.ManageGroups) ?? false);
}
public bool ManagePolicies(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId && o.Permissions.ManagePolicies) ?? false);
}
public bool ManageSso(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId && o.Permissions.ManageSso) ?? false);
}
public bool ManageUsers(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId && o.Permissions.ManageUsers) ?? false);
}
public async Task<ICollection<CurrentContentOrganization>> OrganizationMembershipAsync(
IOrganizationUserRepository organizationUserRepository, Guid userId)
{
@ -196,6 +264,29 @@ namespace Bit.Core
return claims[type].FirstOrDefault()?.Value;
}
private Permissions SetOrganizationPermissionsFromClaims(string organizationId, Dictionary<string, IEnumerable<Claim>> claimsDict)
{
bool hasClaim(string claimKey)
{
return claimsDict.ContainsKey(claimKey) ?
claimsDict[claimKey].Any(x => x.Value == organizationId) : false;
}
return new Permissions
{
AccessBusinessPortal = hasClaim("accessbusinessportal"),
AccessEventLogs = hasClaim("accesseventlogs"),
AccessImportExport = hasClaim(""),
AccessReports = hasClaim("accessreports"),
ManageAllCollections = hasClaim(""),
ManageAssignedCollections = hasClaim(""),
ManageGroups = hasClaim(""),
ManagePolicies = hasClaim(""),
ManageSso = hasClaim("managesso"),
ManageUsers = hasClaim("manageusers")
};
}
public class CurrentContentOrganization
{
public CurrentContentOrganization() { }
@ -204,10 +295,12 @@ namespace Bit.Core
{
Id = orgUser.OrganizationId;
Type = orgUser.Type;
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(orgUser.Permissions);
}
public Guid Id { get; set; }
public OrganizationUserType Type { get; set; }
public Permissions Permissions { get; set; }
}
}
}

View File

@ -6,5 +6,6 @@
Admin = 1,
User = 2,
Manager = 3,
Custom = 4,
}
}

View File

@ -20,7 +20,8 @@ namespace Bit.Core.IdentityServer
"orgowner",
"orgadmin",
"orgmanager",
"orguser"
"orguser",
"orgcustom",
}),
new ApiResource("internal", new string[] { JwtClaimTypes.Subject }),
new ApiResource("api.push", new string[] { JwtClaimTypes.Subject }),

View File

@ -1,4 +1,5 @@
using Bit.Core.Models.Table;
using Bit.Core.Models.Data;
using Bit.Core.Models.Table;
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Api
@ -17,6 +18,8 @@ namespace Bit.Core.Models.Api
[StringLength(50)]
public string BillingEmail { get; set; }
public Permissions Permissions { get; set; }
public virtual Organization ToOrganization(Organization existingOrganization, GlobalSettings globalSettings)
{
if (!globalSettings.SelfHosted)

View File

@ -1,8 +1,9 @@
using Bit.Core.Models.Table;
using Bit.Core.Models.Data;
using Bit.Core.Models.Table;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System;
using System.Linq;
using System.Text.Json;
namespace Bit.Core.Models.Api
{
@ -13,6 +14,7 @@ namespace Bit.Core.Models.Api
[Required]
public Enums.OrganizationUserType? Type { get; set; }
public bool AccessAll { get; set; }
public Permissions Permissions { get; set; }
public IEnumerable<SelectionReadOnlyRequestModel> Collections { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
@ -62,11 +64,16 @@ namespace Bit.Core.Models.Api
[Required]
public Enums.OrganizationUserType? Type { get; set; }
public bool AccessAll { get; set; }
public Permissions Permissions { get; set; }
public IEnumerable<SelectionReadOnlyRequestModel> Collections { get; set; }
public OrganizationUser ToOrganizationUser(OrganizationUser existingUser)
{
existingUser.Type = Type.Value;
existingUser.Permissions = JsonSerializer.Serialize(Permissions, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
});
existingUser.AccessAll = AccessAll;
return existingUser;
}

View File

@ -4,7 +4,7 @@ using Bit.Core.Models.Data;
using System.Collections.Generic;
using System.Linq;
using Bit.Core.Models.Table;
using Bit.Core.Utilities;
namespace Bit.Core.Models.Api
{
public class OrganizationUserResponseModel : ResponseModel
@ -22,6 +22,7 @@ namespace Bit.Core.Models.Api
Type = organizationUser.Type;
Status = organizationUser.Status;
AccessAll = organizationUser.AccessAll;
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(organizationUser.Permissions);
}
public OrganizationUserResponseModel(OrganizationUserUserDetails organizationUser, string obj = "organizationUser")
@ -37,6 +38,7 @@ namespace Bit.Core.Models.Api
Type = organizationUser.Type;
Status = organizationUser.Status;
AccessAll = organizationUser.AccessAll;
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(organizationUser.Permissions);
}
public string Id { get; set; }
@ -44,6 +46,7 @@ namespace Bit.Core.Models.Api
public OrganizationUserType Type { get; set; }
public OrganizationUserStatusType Status { get; set; }
public bool AccessAll { get; set; }
public Permissions Permissions { get; set; }
}
public class OrganizationUserDetailsResponseModel : OrganizationUserResponseModel

View File

@ -1,6 +1,6 @@
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Core.Models.Api
{
public class ProfileOrganizationResponseModel : ResponseModel
@ -29,6 +29,7 @@ namespace Bit.Core.Models.Api
Enabled = organization.Enabled;
SsoBound = !string.IsNullOrWhiteSpace(organization.SsoExternalId);
Identifier = organization.Identifier;
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(organization.Permissions);
}
public string Id { get; set; }
@ -53,5 +54,6 @@ namespace Bit.Core.Models.Api
public bool Enabled { get; set; }
public bool SsoBound { get; set; }
public string Identifier { get; set; }
public Permissions Permissions { get; set; }
}
}

View File

@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.Linq;
using Bit.Core.Models.Api;
using Bit.Core.Models.Data;
namespace Bit.Core.Models.Business
{
public class OrganizationUserInvite
{
public IEnumerable<string> Emails { get; set; }
public Enums.OrganizationUserType? Type { get; set; }
public bool AccessAll { get; set; }
public Permissions Permissions { get; set; }
public IEnumerable<SelectionReadOnly> Collections { get; set; }
public OrganizationUserInvite() {}
public OrganizationUserInvite(OrganizationUserInviteRequestModel requestModel)
{
Emails = requestModel.Emails;
Type = requestModel.Type.Value;
AccessAll = requestModel.AccessAll;
Collections = requestModel.Collections.Select(c => c.ToSelectionReadOnly());
Permissions = requestModel.Permissions;
}
}
}

View File

@ -27,5 +27,6 @@ namespace Bit.Core.Models.Data
public bool Enabled { get; set; }
public string SsoExternalId { get; set; }
public string Identifier { get; set; }
public string Permissions { get; set; }
}
}

View File

@ -21,6 +21,7 @@ namespace Bit.Core.Models.Data
public bool AccessAll { get; set; }
public string ExternalId { get; set; }
public string SsoExternalId { get; set; }
public string Permissions { get; set; }
public Dictionary<TwoFactorProviderType, TwoFactorProvider> GetTwoFactorProviders()
{

View File

@ -0,0 +1,16 @@
namespace Bit.Core.Models.Data
{
public class Permissions
{
public bool AccessBusinessPortal { get; set; }
public bool AccessEventLogs { get; set; }
public bool AccessImportExport { get; set; }
public bool AccessReports { get; set; }
public bool ManageAssignedCollections { get; set; }
public bool ManageAllCollections { get; set; }
public bool ManageGroups { get; set; }
public bool ManagePolicies { get; set; }
public bool ManageSso { get; set; }
public bool ManageUsers { get; set; }
}
}

View File

@ -17,6 +17,7 @@ namespace Bit.Core.Models.Table
public string ExternalId { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
public string Permissions { get; set; }
public void SetNewId()
{

View File

@ -30,11 +30,7 @@ namespace Bit.Core.Services
Task UpdateAsync(Organization organization, bool updateBilling = false);
Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type);
Task DisableTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type);
Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<SelectionReadOnly> collections);
Task<List<OrganizationUser>> InviteUserAsync(Guid organizationId, Guid? invitingUserId,
IEnumerable<string> emails, OrganizationUserType type, bool accessAll, string externalId,
IEnumerable<SelectionReadOnly> collections);
Task<List<OrganizationUser>> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string externalId, OrganizationUserInvite orgUserInvite);
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId);
Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token,
IUserService userService);
@ -44,7 +40,7 @@ namespace Bit.Core.Services
Task SaveUserAsync(OrganizationUser user, Guid? savingUserId, IEnumerable<SelectionReadOnly> collections);
Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId);
Task DeleteUserAsync(Guid organizationId, Guid userId);
Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds);
Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds, Guid? loggedInUserId);
Task<OrganizationLicense> GenerateLicenseAsync(Guid organizationId, Guid installationId);
Task<OrganizationLicense> GenerateLicenseAsync(Organization organization, Guid installationId,
int? version = null);

View File

@ -13,6 +13,7 @@ using Bit.Core.Enums;
using Bit.Core.Models.Data;
using System.IO;
using Newtonsoft.Json;
using System.Text.Json;
namespace Bit.Core.Services
{
@ -972,45 +973,25 @@ namespace Bit.Core.Services
await UpdateAsync(organization);
}
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<SelectionReadOnly> collections)
{
var results = await InviteUserAsync(organizationId, invitingUserId, new List<string> { email },
type, accessAll, externalId, collections);
var result = results.FirstOrDefault();
if (result == null)
{
throw new BadRequestException("This user has already been invited.");
}
return result;
}
public async Task<List<OrganizationUser>> InviteUserAsync(Guid organizationId, Guid? invitingUserId,
IEnumerable<string> emails, OrganizationUserType type, bool accessAll, string externalId,
IEnumerable<SelectionReadOnly> collections)
string externalId, OrganizationUserInvite invite)
{
var organization = await GetOrgById(organizationId);
if (organization == null)
if (organization == null || invite?.Emails == null)
{
throw new NotFoundException();
}
if (type == OrganizationUserType.Owner && invitingUserId.HasValue)
if (invitingUserId.HasValue && invite.Type.HasValue)
{
var invitingUserOrgs = await _organizationUserRepository.GetManyByUserAsync(invitingUserId.Value);
var anyOwners = invitingUserOrgs.Any(
u => u.OrganizationId == organizationId && u.Type == OrganizationUserType.Owner);
if (!anyOwners)
{
throw new BadRequestException("Only owners can invite new owners.");
}
await ValidateOrganizationUserUpdatePermissions(invitingUserId.Value, organizationId, invite.Type.Value, null);
}
if (organization.Seats.HasValue)
{
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organizationId);
var availableSeats = organization.Seats.Value - userCount;
if (availableSeats < emails.Count())
if (availableSeats < invite.Emails.Count())
{
throw new BadRequestException("You have reached the maximum number of users " +
$"({organization.Seats.Value}) for this organization.");
@ -1019,7 +1000,7 @@ namespace Bit.Core.Services
var orgUsers = new List<OrganizationUser>();
var orgUserInvitedCount = 0;
foreach (var email in emails)
foreach (var email in invite.Emails)
{
// Make sure user is not already invited
var existingOrgUserCount = await _organizationUserRepository.GetCountByOrganizationAsync(
@ -1035,17 +1016,21 @@ namespace Bit.Core.Services
UserId = null,
Email = email.ToLowerInvariant(),
Key = null,
Type = type,
Type = invite.Type.Value,
Status = OrganizationUserStatusType.Invited,
AccessAll = accessAll,
AccessAll = invite.AccessAll,
ExternalId = externalId,
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow
RevisionDate = DateTime.UtcNow,
Permissions = System.Text.Json.JsonSerializer.Serialize(invite.Permissions, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
}),
};
if (!orgUser.AccessAll && collections.Any())
if (!orgUser.AccessAll && invite.Collections.Any())
{
await _organizationUserRepository.CreateAsync(orgUser, collections);
await _organizationUserRepository.CreateAsync(orgUser, invite.Collections);
}
else
{
@ -1264,21 +1249,14 @@ namespace Bit.Core.Services
throw new BadRequestException("Invite the user first.");
}
var originalUser = await _organizationUserRepository.GetByIdAsync(user.Id);
if (user.Equals(originalUser)) {
throw new BadRequestException("Please make changes before saving.");
}
if (savingUserId.HasValue)
{
var savingUserOrgs = await _organizationUserRepository.GetManyByUserAsync(savingUserId.Value);
var savingUserIsOrgOwner = savingUserOrgs
.Any(u => u.OrganizationId == user.OrganizationId && u.Type == OrganizationUserType.Owner);
if (!savingUserIsOrgOwner)
{
var originalUser = await _organizationUserRepository.GetByIdAsync(user.Id);
var isOwner = originalUser.Type == OrganizationUserType.Owner;
var nowOwner = user.Type == OrganizationUserType.Owner;
if ((isOwner && !nowOwner) || (!isOwner && nowOwner))
{
throw new BadRequestException("Only an owner can change the user type of another owner.");
}
}
await ValidateOrganizationUserUpdatePermissions(savingUserId.Value, user.OrganizationId, user.Type, originalUser.Type);
}
var confirmedOwners = (await GetConfirmedOwnersAsync(user.OrganizationId)).ToList();
@ -1367,8 +1345,12 @@ namespace Bit.Core.Services
}
}
public async Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds)
public async Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds, Guid? loggedInUserId)
{
if (loggedInUserId.HasValue)
{
await ValidateOrganizationUserUpdatePermissions(loggedInUserId.Value, organizationUser.OrganizationId, organizationUser.Type, null);
}
await _organizationUserRepository.UpdateGroupsAsync(organizationUser.Id, groupIds);
await _eventService.LogOrganizationUserEventAsync(organizationUser,
EventType.OrganizationUser_UpdatedGroups);
@ -1502,8 +1484,16 @@ namespace Bit.Core.Services
try
{
var newUser = await InviteUserAsync(organizationId, importingUserId, user.Email,
OrganizationUserType.User, false, user.ExternalId, new List<SelectionReadOnly>());
var invite = new OrganizationUserInvite
{
Emails = new List<string> { user.Email },
Type = OrganizationUserType.User,
AccessAll = false,
Collections = new List<SelectionReadOnly>(),
};
var newUserPromise = await InviteUserAsync(organizationId, importingUserId, user.ExternalId, invite);
var newUser = newUserPromise.FirstOrDefault();
existingExternalUsersIdDict.Add(newUser.ExternalId, newUser.Id);
}
catch (BadRequestException)
@ -1673,5 +1663,59 @@ namespace Bit.Core.Services
$"{plan.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.");
}
}
private async Task ValidateOrganizationUserUpdatePermissions(Guid loggedInUserId, Guid organizationId, OrganizationUserType newType, OrganizationUserType? oldType)
{
var loggedInUserOrgs = await _organizationUserRepository.GetManyByUserAsync(loggedInUserId);
var loggedInAsOrgOwner = loggedInUserOrgs
.Any(u => u.OrganizationId == organizationId && u.Type == OrganizationUserType.Owner);
if (loggedInAsOrgOwner)
{
return;
}
var isOwner = oldType == OrganizationUserType.Owner;
var nowOwner = newType == OrganizationUserType.Owner;
var ownerUserConfigurationAttempt = (isOwner && nowOwner) || !(isOwner.Equals(nowOwner));
if (ownerUserConfigurationAttempt)
{
throw new BadRequestException("Only an Owner can configure another Owner's account.");
}
var loggedInAsOrgAdmin = loggedInUserOrgs.Any(u => u.OrganizationId == organizationId && u.Type == OrganizationUserType.Admin);
if (loggedInAsOrgAdmin)
{
return;
}
var isCustom = oldType == OrganizationUserType.Custom;
var nowCustom = newType == OrganizationUserType.Custom;
var customUserConfigurationAttempt = (isCustom && nowCustom) || !(isCustom.Equals(nowCustom));
if (customUserConfigurationAttempt)
{
throw new BadRequestException("Only Owners and Admins can configure Custom accounts.");
}
var loggedInAsOrgCustom = loggedInUserOrgs.Any(u => u.OrganizationId == organizationId && u.Type == OrganizationUserType.Custom);
if (!loggedInAsOrgCustom)
{
return;
}
var loggedInCustomOrgUser = loggedInUserOrgs.First(u => u.OrganizationId == organizationId && u.Type == OrganizationUserType.Custom);
var loggedInUserPermissions = CoreHelpers.LoadClassFromJsonData<Permissions>(loggedInCustomOrgUser.Permissions);
if (!loggedInUserPermissions.ManageUsers)
{
throw new BadRequestException("Your account does not have permission to manage users.");
}
var isAdmin = oldType == OrganizationUserType.Admin;
var nowAdmin = newType == OrganizationUserType.Admin;
var adminUserConfigurationAttempt = (isAdmin && nowAdmin) || !(isAdmin.Equals(nowAdmin));
if (adminUserConfigurationAttempt)
{
throw new BadRequestException("Custom users can not manage Admins or Owners.");
}
}
}
}

View File

@ -20,6 +20,7 @@ using Microsoft.Azure.Storage;
using Microsoft.Azure.Storage.Blob;
using Bit.Core.Models.Table;
using IdentityModel;
using System.Text.Json;
namespace Bit.Core.Utilities
{
@ -730,6 +731,62 @@ namespace Bit.Core.Utilities
claims.Add(new KeyValuePair<string, string>("orguser", org.Id.ToString()));
}
break;
case Enums.OrganizationUserType.Custom:
foreach (var org in group)
{
claims.Add(new KeyValuePair<string, string>("orgcustom", org.Id.ToString()));
if (org.Permissions.AccessBusinessPortal)
{
claims.Add(new KeyValuePair<string, string>("accessbusinessportal", org.Id.ToString()));
}
if (org.Permissions.AccessEventLogs)
{
claims.Add(new KeyValuePair<string, string>("accesseventlogs", org.Id.ToString()));
}
if (org.Permissions.AccessImportExport)
{
claims.Add(new KeyValuePair<string, string>("accessimportexport", org.Id.ToString()));
}
if (org.Permissions.AccessReports)
{
claims.Add(new KeyValuePair<string, string>("accessreports", org.Id.ToString()));
}
if (org.Permissions.ManageAllCollections)
{
claims.Add(new KeyValuePair<string, string>("manageallcollections", org.Id.ToString()));
}
if (org.Permissions.ManageAssignedCollections)
{
claims.Add(new KeyValuePair<string, string>("manageassignedcollections", org.Id.ToString()));
}
if (org.Permissions.ManageGroups)
{
claims.Add(new KeyValuePair<string, string>("managegroups", org.Id.ToString()));
}
if (org.Permissions.ManagePolicies)
{
claims.Add(new KeyValuePair<string, string>("managepolicies", org.Id.ToString()));
}
if (org.Permissions.ManageSso)
{
claims.Add(new KeyValuePair<string, string>("managesso", org.Id.ToString()));
}
if (org.Permissions.ManageUsers)
{
claims.Add(new KeyValuePair<string, string>("manageusers", org.Id.ToString()));
}
}
break;
default:
break;
}
@ -737,5 +794,20 @@ namespace Bit.Core.Utilities
}
return claims;
}
public static T LoadClassFromJsonData<T>(string jsonData) where T : new()
{
if (string.IsNullOrWhiteSpace(jsonData))
{
return new T();
}
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
return System.Text.Json.JsonSerializer.Deserialize<T>(jsonData, options);
}
}
}