1
0
mirror of https://github.com/bitwarden/server synced 2026-01-02 08:33:48 +00:00

[PM-24192] Add OrganizationContext in API project (#6291)

This commit is contained in:
Thomas Rittson
2025-09-11 07:37:45 +10:00
committed by GitHub
parent 04cb7820a6
commit bd1745a50d
7 changed files with 238 additions and 12 deletions

View File

@@ -0,0 +1,21 @@
using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Bit.Api.AdminConsole.Authorization;
public static class AuthorizationHandlerCollectionExtensions
{
public static void AddAdminConsoleAuthorizationHandlers(this IServiceCollection services)
{
services.TryAddScoped<IOrganizationContext, OrganizationContext>();
services.TryAddEnumerable([
ServiceDescriptor.Scoped<IAuthorizationHandler, BulkCollectionAuthorizationHandler>(),
ServiceDescriptor.Scoped<IAuthorizationHandler, CollectionAuthorizationHandler>(),
ServiceDescriptor.Scoped<IAuthorizationHandler, GroupAuthorizationHandler>(),
ServiceDescriptor.Scoped<IAuthorizationHandler, OrganizationRequirementHandler>(),
]);
}
}

View File

@@ -1,6 +1,4 @@
#nullable enable
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.AdminConsole.Repositories;

View File

@@ -1,6 +1,4 @@
#nullable enable
using System.Security.Claims;
using System.Security.Claims;
using Bit.Core.Auth.Identity;
using Bit.Core.Context;
using Bit.Core.Enums;

View File

@@ -0,0 +1,84 @@
using System.Security.Claims;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Context;
using Bit.Core.Services;
// Note: do not move this into Core! See remarks below.
namespace Bit.Api.AdminConsole.Authorization;
/// <summary>
/// Provides information about a user's membership or provider relationship with an organization.
/// Used for authorization decisions in the API layer, usually called by a controller or authorization handler or attribute.
/// </summary>
/// <remarks>
/// This is intended to deprecate organization-related methods in <see cref="ICurrentContext"/>.
/// It should remain in the API layer (not Core) because it is closely tied to user claims and authentication.
/// </remarks>
public interface IOrganizationContext
{
/// <summary>
/// Parses the provided <see cref="ClaimsPrincipal"/> for claims relating to the specified organization.
/// A user will have organization claims if they are a confirmed member of the organization.
/// </summary>
/// <param name="user">The claims for the user.</param>
/// <param name="organizationId">The organization to extract claims for.</param>
/// <returns>
/// A <see cref="CurrentContextOrganization"/> representing the user's claims for the organization,
/// or null if the user has no claims.
/// </returns>
public CurrentContextOrganization? GetOrganizationClaims(ClaimsPrincipal user, Guid organizationId);
/// <summary>
/// Used to determine whether the user is a ProviderUser for the specified organization.
/// </summary>
/// <param name="user">The claims for the user.</param>
/// <param name="organizationId">The organization to check the provider relationship for.</param>
/// <returns>True if the user is a ProviderUser for the specified organization, otherwise false.</returns>
/// <remarks>
/// This requires a database call, but the results are cached for the lifetime of the service instance.
/// Try to check purely claims-based sources of authorization first (such as organization membership with
/// <see cref="GetOrganizationClaims"/>) to avoid unnecessary database calls.
/// </remarks>
public Task<bool> IsProviderUserForOrganization(ClaimsPrincipal user, Guid organizationId);
}
public class OrganizationContext(
IUserService userService,
IProviderUserRepository providerUserRepository) : IOrganizationContext
{
public const string NoUserIdError = "This method should only be called on the private api with a logged in user.";
/// <summary>
/// Caches provider relationships by UserId.
/// In practice this should only have 1 entry (for the current user), but this approach ensures that a mix-up
/// between users cannot occur if <see cref="IsProviderUserForOrganization"/> is called with a different
/// ClaimsPrincipal for any reason.
/// </summary>
private readonly Dictionary<Guid, IEnumerable<ProviderUserOrganizationDetails>> _providerUserOrganizationsCache = new();
public CurrentContextOrganization? GetOrganizationClaims(ClaimsPrincipal user, Guid organizationId)
{
return user.GetCurrentContextOrganization(organizationId);
}
public async Task<bool> IsProviderUserForOrganization(ClaimsPrincipal user, Guid organizationId)
{
var userId = userService.GetProperUserId(user);
if (!userId.HasValue)
{
throw new InvalidOperationException(NoUserIdError);
}
if (!_providerUserOrganizationsCache.TryGetValue(userId.Value, out var providerUserOrganizations))
{
providerUserOrganizations =
await providerUserRepository.GetManyOrganizationDetailsByUserAsync(userId.Value,
ProviderUserStatusType.Confirmed);
providerUserOrganizations = providerUserOrganizations.ToList();
_providerUserOrganizationsCache[userId.Value] = providerUserOrganizations;
}
return providerUserOrganizations.Any(o => o.OrganizationId == organizationId);
}
}