1
0
mirror of https://github.com/bitwarden/server synced 2025-12-12 22:33:45 +00:00

[PM-12474] Move to authorization to attibutes/handlers/requirements (#6001)

* Created ReadAllOrganizationUsersBasicInformationRequirement for use with Authorize attribute.

* Removed unused req and Handler and tests. Moved to new auth attribute

* Moved tests to integration tests with new response.

* Removed tests that were migrated to integration tests.

* Made string params Guids instead of parsing them manually in methods.

* Admin and Owner added to requirement.

* Added XML docs for basic get endpoint. Removed unused. Added another auth check. Inverted if check.

* Removed unused endpoint

* Added tests for requirement

* Added checks for both User and Custom

* Added org id check to validate the user being requested belongs to the org in the route.

* typo
This commit is contained in:
Jared McCannon
2025-07-15 07:52:47 -05:00
committed by GitHub
parent 93a00373d2
commit c4965350d1
8 changed files with 253 additions and 307 deletions

View File

@@ -1,6 +1,7 @@
// 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.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations;
@@ -126,10 +127,11 @@ public class OrganizationUsersController : Controller
}
[HttpGet("{id}")]
public async Task<OrganizationUserDetailsResponseModel> Get(Guid id, bool includeGroups = false)
[Authorize<ManageUsersRequirement>]
public async Task<OrganizationUserDetailsResponseModel> Get(Guid orgId, Guid id, bool includeGroups = false)
{
var (organizationUser, collections) = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id);
if (organizationUser == null || !await _currentContext.ManageUsers(organizationUser.OrganizationId))
if (organizationUser == null || organizationUser.OrganizationId != orgId)
{
throw new NotFoundException();
}
@@ -148,16 +150,17 @@ public class OrganizationUsersController : Controller
return response;
}
/// <summary>
/// Returns a set of basic information about all members of the organization. This is available to all members of
/// the organization to manage collections. For this reason, it contains as little information as possible and no
/// cryptographic keys or other sensitive data.
/// </summary>
/// <param name="orgId">Organization identifier</param>
/// <returns>List of users for the organization.</returns>
[HttpGet("mini-details")]
[Authorize<MemberOrProviderRequirement>]
public async Task<ListResponseModel<OrganizationUserUserMiniDetailsResponseModel>> GetMiniDetails(Guid orgId)
{
var authorizationResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId),
OrganizationUserUserMiniDetailsOperations.ReadAll);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
var organizationUserUserDetails = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(orgId);
return new ListResponseModel<OrganizationUserUserMiniDetailsResponseModel>(
organizationUserUserDetails.Select(ou => new OrganizationUserUserMiniDetailsResponseModel(ou)));
@@ -207,7 +210,7 @@ public class OrganizationUsersController : Controller
{
OrganizationId = orgId,
IncludeGroups = includeGroups,
IncludeCollections = includeCollections,
IncludeCollections = includeCollections
};
if ((await _authorizationService.AuthorizeAsync(User, new ManageUsersRequirement())).Succeeded)
@@ -231,34 +234,12 @@ public class OrganizationUsersController : Controller
.ToList());
}
[HttpGet("{id}/groups")]
public async Task<IEnumerable<string>> GetGroups(string orgId, string id)
{
var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id));
if (organizationUser == null || (!await _currentContext.ManageGroups(organizationUser.OrganizationId) &&
!await _currentContext.ManageUsers(organizationUser.OrganizationId)))
{
throw new NotFoundException();
}
var groupIds = await _groupRepository.GetManyIdsByUserIdAsync(organizationUser.Id);
var responses = groupIds.Select(g => g.ToString());
return responses;
}
[HttpGet("{id}/reset-password-details")]
public async Task<OrganizationUserResetPasswordDetailsResponseModel> GetResetPasswordDetails(string orgId, string id)
[Authorize<ManageAccountRecoveryRequirement>]
public async Task<OrganizationUserResetPasswordDetailsResponseModel> GetResetPasswordDetails(Guid orgId, Guid id)
{
// Make sure the calling user can reset passwords for this org
var orgGuidId = new Guid(orgId);
if (!await _currentContext.ManageResetPassword(orgGuidId))
{
throw new NotFoundException();
}
var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id));
if (organizationUser == null || !organizationUser.UserId.HasValue)
var organizationUser = await _organizationUserRepository.GetByIdAsync(id);
if (organizationUser is null || organizationUser.UserId is null)
{
throw new NotFoundException();
}
@@ -272,7 +253,7 @@ public class OrganizationUsersController : Controller
}
// Retrieve Encrypted Private Key from organization
var org = await _organizationRepository.GetByIdAsync(orgGuidId);
var org = await _organizationRepository.GetByIdAsync(orgId);
if (org == null)
{
throw new NotFoundException();
@@ -282,26 +263,17 @@ public class OrganizationUsersController : Controller
}
[HttpPost("account-recovery-details")]
[Authorize<ManageAccountRecoveryRequirement>]
public async Task<ListResponseModel<OrganizationUserResetPasswordDetailsResponseModel>> GetAccountRecoveryDetails(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
{
// Make sure the calling user can reset passwords for this org
if (!await _currentContext.ManageResetPassword(orgId))
{
throw new NotFoundException();
}
var responses = await _organizationUserRepository.GetManyAccountRecoveryDetailsByOrganizationUserAsync(orgId, model.Ids);
return new ListResponseModel<OrganizationUserResetPasswordDetailsResponseModel>(responses.Select(r => new OrganizationUserResetPasswordDetailsResponseModel(r)));
}
[HttpPost("invite")]
[Authorize<ManageUsersRequirement>]
public async Task Invite(Guid orgId, [FromBody] OrganizationUserInviteRequestModel model)
{
if (!await _currentContext.ManageUsers(orgId))
{
throw new NotFoundException();
}
// Check the user has permission to grant access to the collections for the new user
if (model.Collections?.Any() == true)
{
@@ -317,35 +289,25 @@ public class OrganizationUsersController : Controller
var userId = _userService.GetProperUserId(User);
await _organizationService.InviteUsersAsync(orgId, userId.Value, systemUser: null,
new (OrganizationUserInvite, string)[] { (new OrganizationUserInvite(model.ToData()), null) });
[(new OrganizationUserInvite(model.ToData()), null)]);
}
[HttpPost("reinvite")]
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkReinvite(string orgId, [FromBody] OrganizationUserBulkRequestModel model)
[Authorize<ManageUsersRequirement>]
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkReinvite(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
{
var orgGuidId = new Guid(orgId);
if (!await _currentContext.ManageUsers(orgGuidId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User);
var result = await _organizationService.ResendInvitesAsync(orgGuidId, userId.Value, model.Ids);
var result = await _organizationService.ResendInvitesAsync(orgId, userId.Value, model.Ids);
return new ListResponseModel<OrganizationUserBulkResponseModel>(
result.Select(t => new OrganizationUserBulkResponseModel(t.Item1.Id, t.Item2)));
}
[HttpPost("{id}/reinvite")]
public async Task Reinvite(string orgId, string id)
[Authorize<ManageUsersRequirement>]
public async Task Reinvite(Guid orgId, Guid id)
{
var orgGuidId = new Guid(orgId);
if (!await _currentContext.ManageUsers(orgGuidId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User);
await _organizationService.ResendInviteAsync(orgGuidId, userId.Value, new Guid(id));
await _organizationService.ResendInviteAsync(orgId, userId.Value, id);
}
[HttpPost("{organizationUserId}/accept-init")]
@@ -406,57 +368,39 @@ public class OrganizationUsersController : Controller
}
[HttpPost("{id}/confirm")]
[Authorize<ManageUsersRequirement>]
public async Task Confirm(Guid orgId, Guid id, [FromBody] OrganizationUserConfirmRequestModel model)
{
if (!await _currentContext.ManageUsers(orgId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User);
var result = await _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, id, model.Key, userId.Value, model.DefaultUserCollectionName);
_ = await _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, id, model.Key, userId.Value, model.DefaultUserCollectionName);
}
[HttpPost("confirm")]
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkConfirm(string orgId,
[Authorize<ManageUsersRequirement>]
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkConfirm(Guid orgId,
[FromBody] OrganizationUserBulkConfirmRequestModel model)
{
var orgGuidId = new Guid(orgId);
if (!await _currentContext.ManageUsers(orgGuidId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User);
var results = await _confirmOrganizationUserCommand.ConfirmUsersAsync(orgGuidId, model.ToDictionary(), userId.Value);
var results = await _confirmOrganizationUserCommand.ConfirmUsersAsync(orgId, model.ToDictionary(), userId.Value);
return new ListResponseModel<OrganizationUserBulkResponseModel>(results.Select(r =>
new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2)));
}
[HttpPost("public-keys")]
public async Task<ListResponseModel<OrganizationUserPublicKeyResponseModel>> UserPublicKeys(string orgId, [FromBody] OrganizationUserBulkRequestModel model)
[Authorize<ManageUsersRequirement>]
public async Task<ListResponseModel<OrganizationUserPublicKeyResponseModel>> UserPublicKeys(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
{
var orgGuidId = new Guid(orgId);
if (!await _currentContext.ManageUsers(orgGuidId))
{
throw new NotFoundException();
}
var result = await _organizationUserRepository.GetManyPublicKeysByOrganizationUserAsync(orgGuidId, model.Ids);
var result = await _organizationUserRepository.GetManyPublicKeysByOrganizationUserAsync(orgId, model.Ids);
var responses = result.Select(r => new OrganizationUserPublicKeyResponseModel(r.Id, r.UserId, r.PublicKey)).ToList();
return new ListResponseModel<OrganizationUserPublicKeyResponseModel>(responses);
}
[HttpPut("{id}")]
[HttpPost("{id}")]
[Authorize<ManageUsersRequirement>]
public async Task Put(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model)
{
if (!await _currentContext.ManageUsers(orgId))
{
throw new NotFoundException();
}
var (organizationUser, currentAccess) = await _organizationUserRepository.GetByIdWithCollectionsAsync(id);
if (organizationUser == null || organizationUser.OrganizationId != orgId)
{
@@ -557,27 +501,19 @@ public class OrganizationUsersController : Controller
}
[HttpPut("{id}/reset-password")]
public async Task PutResetPassword(string orgId, string id, [FromBody] OrganizationUserResetPasswordRequestModel model)
[Authorize<ManageAccountRecoveryRequirement>]
public async Task PutResetPassword(Guid orgId, Guid id, [FromBody] OrganizationUserResetPasswordRequestModel model)
{
var orgGuidId = new Guid(orgId);
// Calling user must have Manage Reset Password permission
if (!await _currentContext.ManageResetPassword(orgGuidId))
{
throw new NotFoundException();
}
// Get the users role, since provider users aren't a member of the organization we use the owner check
var orgUserType = await _currentContext.OrganizationOwner(orgGuidId)
var orgUserType = await _currentContext.OrganizationOwner(orgId)
? OrganizationUserType.Owner
: _currentContext.Organizations?.FirstOrDefault(o => o.Id == orgGuidId)?.Type;
: _currentContext.Organizations?.FirstOrDefault(o => o.Id == orgId)?.Type;
if (orgUserType == null)
{
throw new NotFoundException();
}
var result = await _userService.AdminResetPasswordAsync(orgUserType.Value, orgGuidId, new Guid(id), model.NewMasterPasswordHash, model.Key);
var result = await _userService.AdminResetPasswordAsync(orgUserType.Value, orgId, id, model.NewMasterPasswordHash, model.Key);
if (result.Succeeded)
{
return;
@@ -594,26 +530,18 @@ public class OrganizationUsersController : Controller
[HttpDelete("{id}")]
[HttpPost("{id}/remove")]
[Authorize<ManageUsersRequirement>]
public async Task Remove(Guid orgId, Guid id)
{
if (!await _currentContext.ManageUsers(orgId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User);
await _removeOrganizationUserCommand.RemoveUserAsync(orgId, id, userId.Value);
}
[HttpDelete("")]
[HttpPost("remove")]
[Authorize<ManageUsersRequirement>]
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRemove(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
{
if (!await _currentContext.ManageUsers(orgId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User);
var result = await _removeOrganizationUserCommand.RemoveUsersAsync(orgId, model.Ids, userId.Value);
return new ListResponseModel<OrganizationUserBulkResponseModel>(result.Select(r =>
@@ -622,13 +550,9 @@ public class OrganizationUsersController : Controller
[HttpDelete("{id}/delete-account")]
[HttpPost("{id}/delete-account")]
[Authorize<ManageUsersRequirement>]
public async Task DeleteAccount(Guid orgId, Guid id)
{
if (!await _currentContext.ManageUsers(orgId))
{
throw new NotFoundException();
}
var currentUser = await _userService.GetUserByPrincipalAsync(User);
if (currentUser == null)
{
@@ -640,13 +564,9 @@ public class OrganizationUsersController : Controller
[HttpDelete("delete-account")]
[HttpPost("delete-account")]
[Authorize<ManageUsersRequirement>]
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
{
if (!await _currentContext.ManageUsers(orgId))
{
throw new NotFoundException();
}
var currentUser = await _userService.GetUserByPrincipalAsync(User);
if (currentUser == null)
{
@@ -661,6 +581,7 @@ public class OrganizationUsersController : Controller
[HttpPatch("{id}/revoke")]
[HttpPut("{id}/revoke")]
[Authorize<ManageUsersRequirement>]
public async Task RevokeAsync(Guid orgId, Guid id)
{
await RestoreOrRevokeUserAsync(orgId, id, _organizationService.RevokeUserAsync);
@@ -668,6 +589,7 @@ public class OrganizationUsersController : Controller
[HttpPatch("revoke")]
[HttpPut("revoke")]
[Authorize<ManageUsersRequirement>]
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRevokeAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
{
return await RestoreOrRevokeUsersAsync(orgId, model, _organizationService.RevokeUsersAsync);
@@ -675,6 +597,7 @@ public class OrganizationUsersController : Controller
[HttpPatch("{id}/restore")]
[HttpPut("{id}/restore")]
[Authorize<ManageUsersRequirement>]
public async Task RestoreAsync(Guid orgId, Guid id)
{
await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _restoreOrganizationUserCommand.RestoreUserAsync(orgUser, userId));
@@ -682,6 +605,7 @@ public class OrganizationUsersController : Controller
[HttpPatch("restore")]
[HttpPut("restore")]
[Authorize<ManageUsersRequirement>]
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRestoreAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
{
return await RestoreOrRevokeUsersAsync(orgId, model, (orgId, orgUserIds, restoringUserId) => _restoreOrganizationUserCommand.RestoreUsersAsync(orgId, orgUserIds, restoringUserId, _userService));
@@ -689,14 +613,10 @@ public class OrganizationUsersController : Controller
[HttpPatch("enable-secrets-manager")]
[HttpPut("enable-secrets-manager")]
[Authorize<ManageUsersRequirement>]
public async Task BulkEnableSecretsManagerAsync(Guid orgId,
[FromBody] OrganizationUserBulkRequestModel model)
{
if (!await _currentContext.ManageUsers(orgId))
{
throw new NotFoundException();
}
var orgUsers = (await _organizationUserRepository.GetManyAsync(model.Ids))
.Where(ou => ou.OrganizationId == orgId && !ou.AccessSecretsManager).ToList();
if (orgUsers.Count == 0)
@@ -729,11 +649,6 @@ public class OrganizationUsersController : Controller
Guid id,
Func<Core.Entities.OrganizationUser, Guid?, Task> statusAction)
{
if (!await _currentContext.ManageUsers(orgId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User);
var orgUser = await _organizationUserRepository.GetByIdAsync(id);
if (orgUser == null || orgUser.OrganizationId != orgId)
@@ -749,11 +664,6 @@ public class OrganizationUsersController : Controller
OrganizationUserBulkRequestModel model,
Func<Guid, IEnumerable<Guid>, Guid?, Task<List<Tuple<Core.Entities.OrganizationUser, string>>>> statusAction)
{
if (!await _currentContext.ManageUsers(orgId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User);
var result = await statusAction(orgId, model.Ids, userId.Value);
return new ListResponseModel<OrganizationUserBulkResponseModel>(result.Select(r =>