1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-04 01:22:50 -05:00

[AC-292] Public Api - allow configuration of custom permissions (#4022)

* Also refactor OrganizationService user invite methods
This commit is contained in:
Thomas Rittson
2024-05-31 09:23:31 +10:00
committed by GitHub
parent 0189952e1f
commit 357ac4f40a
23 changed files with 829 additions and 179 deletions

View File

@ -35,4 +35,9 @@ public class OrganizationUser : ITableObject<Guid>, IExternal
return string.IsNullOrWhiteSpace(Permissions) ? null
: CoreHelpers.LoadClassFromJsonData<Permissions>(Permissions);
}
public void SetPermissions(Permissions permissions)
{
Permissions = CoreHelpers.ClassToJsonData(permissions);
}
}

View File

@ -43,14 +43,10 @@ public interface IOrganizationService
Task UpdateAsync(Organization organization, bool updateBilling = false, EventType eventType = EventType.Organization_Updated);
Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type);
Task DisableTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type);
Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, Guid? invitingUserId,
Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser,
OrganizationUserInvite invite, string externalId);
Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser,
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites);
Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, EventSystemUser systemUser,
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites);
Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
OrganizationUserType type, bool accessAll, string externalId, ICollection<CollectionAccessSelection> collections, IEnumerable<Guid> groups);
Task<OrganizationUser> InviteUserAsync(Guid organizationId, EventSystemUser systemUser, string email,
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections, IEnumerable<Guid> groups, bool accessSecretsManager);
Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId);
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false);
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,

View File

@ -957,12 +957,53 @@ public class OrganizationService : IOrganizationService
await UpdateAsync(organization);
}
public async Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, Guid? invitingUserId,
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser,
OrganizationUserInvite invite, string externalId)
{
// Ideally OrganizationUserInvite should represent a single user so that this doesn't have to be a runtime check
if (invite.Emails.Count() > 1)
{
throw new BadRequestException("This method can only be used to invite a single user.");
}
// Validate Collection associations if org is using latest collection enhancements
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
if (organizationAbility?.FlexibleCollections ?? false)
{
var invalidAssociations = invite.Collections?.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords));
if (invalidAssociations?.Any() ?? false)
{
throw new BadRequestException("The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true.");
}
}
var results = await InviteUsersAsync(organizationId, invitingUserId, systemUser,
new (OrganizationUserInvite, string)[] { (invite, externalId) });
var result = results.FirstOrDefault();
if (result == null)
{
throw new BadRequestException("This user has already been invited.");
}
return result;
}
/// <summary>
/// Invite users to an organization.
/// </summary>
/// <param name="organizationId">The organization Id</param>
/// <param name="invitingUserId">The current authenticated user who is sending the invite. Only used when inviting via a client app; null if using SCIM or Public API.</param>
/// <param name="systemUser">The system user which is sending the invite. Only used when inviting via SCIM; null if using a client app or Public API</param>
/// <param name="invites">Details about the users being invited</param>
/// <returns></returns>
public async Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser,
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites)
{
var inviteTypes = new HashSet<OrganizationUserType>(invites.Where(i => i.invite.Type.HasValue)
.Select(i => i.invite.Type.Value));
// If authenticating via a client app, verify the inviting user has permissions
// cf. SCIM and Public API have superuser permissions here
if (invitingUserId.HasValue && inviteTypes.Count > 0)
{
foreach (var (invite, _) in invites)
@ -972,25 +1013,24 @@ public class OrganizationService : IOrganizationService
}
}
var (organizationUsers, events) = await SaveUsersSendInvitesAsync(organizationId, invites, systemUser: null);
var (organizationUsers, events) = await SaveUsersSendInvitesAsync(organizationId, invites);
await _eventService.LogOrganizationUserEventsAsync(events);
return organizationUsers;
}
public async Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, EventSystemUser systemUser,
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites)
{
var (organizationUsers, events) = await SaveUsersSendInvitesAsync(organizationId, invites, systemUser);
await _eventService.LogOrganizationUserEventsAsync(events.Select(e => (e.Item1, e.Item2, systemUser, e.Item3)));
if (systemUser.HasValue)
{
// Log SCIM event
await _eventService.LogOrganizationUserEventsAsync(events.Select(e => (e.Item1, e.Item2, systemUser.Value, e.Item3)));
}
else
{
// Log client app or Public Api event
await _eventService.LogOrganizationUserEventsAsync(events);
}
return organizationUsers;
}
private async Task<(List<OrganizationUser> organizationUsers, List<(OrganizationUser, EventType, DateTime?)> events)> SaveUsersSendInvitesAsync(Guid organizationId,
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites, EventSystemUser? systemUser)
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites)
{
var organization = await GetOrgById(organizationId);
var initialSeatCount = organization.Seats;
@ -1087,9 +1127,9 @@ public class OrganizationService : IOrganizationService
RevisionDate = DateTime.UtcNow,
};
if (invite.Permissions != null)
if (invite.Type == OrganizationUserType.Custom)
{
orgUser.Permissions = JsonSerializer.Serialize(invite.Permissions, JsonHelpers.CamelCase);
orgUser.SetPermissions(invite.Permissions ?? new Permissions());
}
if (!orgUser.AccessAll && invite.Collections.Any())
@ -1667,55 +1707,6 @@ public class OrganizationService : IOrganizationService
EventType.OrganizationUser_ResetPassword_Enroll : EventType.OrganizationUser_ResetPassword_Withdraw);
}
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
OrganizationUserType type, bool accessAll, string externalId, ICollection<CollectionAccessSelection> collections,
IEnumerable<Guid> groups)
{
// Validate Collection associations if org is using latest collection enhancements
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
if (organizationAbility?.FlexibleCollections ?? false)
{
var invalidAssociations = collections?.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords));
if (invalidAssociations?.Any() ?? false)
{
throw new BadRequestException("The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true.");
}
}
return await SaveUserSendInviteAsync(organizationId, invitingUserId, systemUser: null, email, type, accessAll, externalId, collections, groups);
}
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, EventSystemUser systemUser, string email,
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections,
IEnumerable<Guid> groups, bool accessSecretsManager)
{
// Collection associations validation not required as they are always an empty list - created via system user (scim)
return await SaveUserSendInviteAsync(organizationId, invitingUserId: null, systemUser, email, type, accessAll, externalId, collections, groups, accessSecretsManager);
}
private async Task<OrganizationUser> SaveUserSendInviteAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser, string email,
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections, IEnumerable<Guid> groups, bool accessSecretsManager = false)
{
var invite = new OrganizationUserInvite()
{
Emails = new List<string> { email },
Type = type,
AccessAll = accessAll,
Collections = collections,
Groups = groups,
AccessSecretsManager = accessSecretsManager
};
var results = systemUser.HasValue ? await InviteUsersAsync(organizationId, systemUser.Value,
new (OrganizationUserInvite, string)[] { (invite, externalId) }) : await InviteUsersAsync(organizationId, invitingUserId,
new (OrganizationUserInvite, string)[] { (invite, externalId) });
var result = results.FirstOrDefault();
if (result == null)
{
throw new BadRequestException("This user has already been invited.");
}
return result;
}
public async Task ImportAsync(Guid organizationId,
Guid? importingUserId,
IEnumerable<ImportedGroup> groups,
@ -1831,7 +1822,7 @@ public class OrganizationService : IOrganizationService
}
}
var invitedUsers = await InviteUsersAsync(organizationId, importingUserId, userInvites);
var invitedUsers = await InviteUsersAsync(organizationId, importingUserId, systemUser: null, userInvites);
foreach (var invitedUser in invitedUsers)
{
existingExternalUsersIdDict.Add(invitedUser.ExternalId, invitedUser.Id);