mirror of
https://github.com/bitwarden/server
synced 2025-12-21 02:33:30 +00:00
[PM-24192] Add OrganizationContext in API project (#6291)
This commit is contained in:
@@ -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>(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.Models.Data.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
#nullable enable
|
using System.Security.Claims;
|
||||||
|
|
||||||
using System.Security.Claims;
|
|
||||||
using Bit.Core.Auth.Identity;
|
using Bit.Core.Auth.Identity;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
|||||||
84
src/Api/AdminConsole/Authorization/OrganizationContext.cs
Normal file
84
src/Api/AdminConsole/Authorization/OrganizationContext.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
using Bit.Api.AdminConsole.Authorization;
|
using Bit.Api.AdminConsole.Authorization;
|
||||||
using Bit.Api.Tools.Authorization;
|
using Bit.Api.Tools.Authorization;
|
||||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
|
|
||||||
using Bit.Core.Auth.IdentityServer;
|
using Bit.Core.Auth.IdentityServer;
|
||||||
using Bit.Core.PhishingDomainFeatures;
|
using Bit.Core.PhishingDomainFeatures;
|
||||||
using Bit.Core.PhishingDomainFeatures.Interfaces;
|
using Bit.Core.PhishingDomainFeatures.Interfaces;
|
||||||
@@ -109,14 +107,12 @@ public static class ServiceCollectionExtensions
|
|||||||
|
|
||||||
public static void AddAuthorizationHandlers(this IServiceCollection services)
|
public static void AddAuthorizationHandlers(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddScoped<IAuthorizationHandler, BulkCollectionAuthorizationHandler>();
|
|
||||||
services.AddScoped<IAuthorizationHandler, CollectionAuthorizationHandler>();
|
|
||||||
services.AddScoped<IAuthorizationHandler, GroupAuthorizationHandler>();
|
|
||||||
services.AddScoped<IAuthorizationHandler, VaultExportAuthorizationHandler>();
|
services.AddScoped<IAuthorizationHandler, VaultExportAuthorizationHandler>();
|
||||||
services.AddScoped<IAuthorizationHandler, SecurityTaskAuthorizationHandler>();
|
services.AddScoped<IAuthorizationHandler, SecurityTaskAuthorizationHandler>();
|
||||||
services.AddScoped<IAuthorizationHandler, SecurityTaskOrganizationAuthorizationHandler>();
|
services.AddScoped<IAuthorizationHandler, SecurityTaskOrganizationAuthorizationHandler>();
|
||||||
|
|
||||||
services.AddScoped<IAuthorizationHandler, OrganizationRequirementHandler>();
|
// Admin Console authorization handlers
|
||||||
|
services.AddAdminConsoleAuthorizationHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddPhishingDomainServices(this IServiceCollection services, GlobalSettings globalSettings)
|
public static void AddPhishingDomainServices(this IServiceCollection services, GlobalSettings globalSettings)
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ using Bit.Core.Utilities;
|
|||||||
|
|
||||||
namespace Bit.Core.Context;
|
namespace Bit.Core.Context;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the claims for a user in relation to a particular organization.
|
||||||
|
/// These claims will only be present for users in the <see cref="OrganizationUserStatusType.Confirmed"/> status.
|
||||||
|
/// </summary>
|
||||||
public class CurrentContextOrganization
|
public class CurrentContextOrganization
|
||||||
{
|
{
|
||||||
public CurrentContextOrganization() { }
|
public CurrentContextOrganization() { }
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using AutoFixture;
|
||||||
|
using Bit.Api.AdminConsole.Authorization;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Api.Test.AdminConsole.Authorization;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class OrganizationContextTests
|
||||||
|
{
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task IsProviderUserForOrganization_UserIsProviderUser_ReturnsTrue(
|
||||||
|
Guid userId, Guid organizationId, Guid otherOrganizationId,
|
||||||
|
SutProvider<OrganizationContext> sutProvider)
|
||||||
|
{
|
||||||
|
var claimsPrincipal = new ClaimsPrincipal();
|
||||||
|
var providerUserOrganizations = new List<ProviderUserOrganizationDetails>
|
||||||
|
{
|
||||||
|
new() { OrganizationId = organizationId },
|
||||||
|
new() { OrganizationId = otherOrganizationId }
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.GetProperUserId(claimsPrincipal)
|
||||||
|
.Returns(userId);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IProviderUserRepository>()
|
||||||
|
.GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed)
|
||||||
|
.Returns(providerUserOrganizations);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.IsProviderUserForOrganization(claimsPrincipal, organizationId);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
await sutProvider.GetDependency<IProviderUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<object[]> UserIsNotProviderUserData()
|
||||||
|
{
|
||||||
|
// User has provider organizations, but not for the target organization
|
||||||
|
yield return
|
||||||
|
[
|
||||||
|
new List<ProviderUserOrganizationDetails>
|
||||||
|
{
|
||||||
|
new Fixture().Create<ProviderUserOrganizationDetails>()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// User has no provider organizations
|
||||||
|
yield return [Array.Empty<ProviderUserOrganizationDetails>()];
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitMemberAutoData(nameof(UserIsNotProviderUserData))]
|
||||||
|
public async Task IsProviderUserForOrganization_UserIsNotProviderUser_ReturnsFalse(
|
||||||
|
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizations,
|
||||||
|
Guid userId, Guid organizationId,
|
||||||
|
SutProvider<OrganizationContext> sutProvider)
|
||||||
|
{
|
||||||
|
var claimsPrincipal = new ClaimsPrincipal();
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.GetProperUserId(claimsPrincipal)
|
||||||
|
.Returns(userId);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IProviderUserRepository>()
|
||||||
|
.GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed)
|
||||||
|
.Returns(providerUserOrganizations);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.IsProviderUserForOrganization(claimsPrincipal, organizationId);
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task IsProviderUserForOrganization_UserIdIsNull_ThrowsException(
|
||||||
|
Guid organizationId,
|
||||||
|
SutProvider<OrganizationContext> sutProvider)
|
||||||
|
{
|
||||||
|
var claimsPrincipal = new ClaimsPrincipal();
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.GetProperUserId(claimsPrincipal)
|
||||||
|
.Returns((Guid?)null);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||||
|
sutProvider.Sut.IsProviderUserForOrganization(claimsPrincipal, organizationId));
|
||||||
|
|
||||||
|
Assert.Equal(OrganizationContext.NoUserIdError, exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task IsProviderUserForOrganization_UsesCaching(
|
||||||
|
Guid userId, Guid organizationId,
|
||||||
|
SutProvider<OrganizationContext> sutProvider)
|
||||||
|
{
|
||||||
|
var claimsPrincipal = new ClaimsPrincipal();
|
||||||
|
var providerUserOrganizations = new List<ProviderUserOrganizationDetails>
|
||||||
|
{
|
||||||
|
new() { OrganizationId = organizationId }
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.GetProperUserId(claimsPrincipal)
|
||||||
|
.Returns(userId);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IProviderUserRepository>()
|
||||||
|
.GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed)
|
||||||
|
.Returns(providerUserOrganizations);
|
||||||
|
|
||||||
|
await sutProvider.Sut.IsProviderUserForOrganization(claimsPrincipal, organizationId);
|
||||||
|
await sutProvider.Sut.IsProviderUserForOrganization(claimsPrincipal, organizationId);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IProviderUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user