fix: scope organization access by membership
All checks were successful
deploy-socialize / image (push) Successful in 54s
deploy-socialize / deploy (push) Successful in 19s

This commit is contained in:
2026-05-08 09:09:16 -04:00
parent c527011646
commit e81c9f42c9
8 changed files with 101 additions and 110 deletions

View File

@@ -24,7 +24,7 @@ internal sealed class AccessScopeService(
public static bool CanAccessWorkspace(ClaimsPrincipal user, Guid workspaceId) public static bool CanAccessWorkspace(ClaimsPrincipal user, Guid workspaceId)
{ {
return IsManager(user) || user.GetWorkspaceScopeIds().Contains(workspaceId); return user.GetWorkspaceScopeIds().Contains(workspaceId);
} }
public static bool CanManageWorkspace(ClaimsPrincipal user, Guid workspaceId) public static bool CanManageWorkspace(ClaimsPrincipal user, Guid workspaceId)
@@ -34,24 +34,25 @@ internal sealed class AccessScopeService(
public static bool CanAccessClient(ClaimsPrincipal user, Guid workspaceId, Guid clientId) public static bool CanAccessClient(ClaimsPrincipal user, Guid workspaceId, Guid clientId)
{ {
return IsManager(user) return CanAccessWorkspace(user, workspaceId) &&
|| (CanAccessWorkspace(user, workspaceId) && user.GetClientScopeIds().Contains(clientId)); (IsManager(user) || user.GetClientScopeIds().Contains(clientId));
} }
public static bool CanAccessCampaign(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId) public static bool CanAccessCampaign(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
{ {
return IsManager(user) return CanAccessClient(user, workspaceId, clientId) &&
|| (CanAccessClient(user, workspaceId, clientId) && user.GetCampaignScopeIds().Contains(campaignId)); (IsManager(user) || user.GetCampaignScopeIds().Contains(campaignId));
} }
public static bool CanContributeToCampaign(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId) public static bool CanContributeToCampaign(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
{ {
return IsManager(user) || (IsProvider(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId)); return IsManager(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId)
|| IsProvider(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId);
} }
public static bool CanReviewContent(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId) public static bool CanReviewContent(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
{ {
return IsManager(user) return IsManager(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId)
|| IsProvider(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId) || IsProvider(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId)
|| IsClient(user) && CanAccessClient(user, workspaceId, clientId); || IsClient(user) && CanAccessClient(user, workspaceId, clientId);
} }
@@ -68,7 +69,7 @@ internal sealed class AccessScopeService(
Guid workspaceId, Guid workspaceId,
CancellationToken ct) CancellationToken ct)
{ {
return CanAccessWorkspace(user, workspaceId) return user.GetWorkspaceScopeIds().Contains(workspaceId)
|| await organizationAccessService.HasInheritedWorkspacePermissionAsync( || await organizationAccessService.HasInheritedWorkspacePermissionAsync(
user, user,
workspaceId, workspaceId,
@@ -81,7 +82,7 @@ internal sealed class AccessScopeService(
Guid workspaceId, Guid workspaceId,
CancellationToken ct) CancellationToken ct)
{ {
return IsManager(user) return CanManageWorkspace(user, workspaceId)
|| await organizationAccessService.HasInheritedWorkspacePermissionAsync( || await organizationAccessService.HasInheritedWorkspacePermissionAsync(
user, user,
workspaceId, workspaceId,
@@ -94,8 +95,7 @@ internal sealed class AccessScopeService(
Guid organizationId, Guid organizationId,
CancellationToken ct) CancellationToken ct)
{ {
return IsManager(user) return await organizationAccessService.HasOrganizationPermissionAsync(
|| await organizationAccessService.HasOrganizationPermissionAsync(
user, user,
organizationId, organizationId,
OrganizationPermissions.CreateWorkspaces, OrganizationPermissions.CreateWorkspaces,
@@ -108,8 +108,7 @@ internal sealed class AccessScopeService(
Guid clientId, Guid clientId,
CancellationToken ct) CancellationToken ct)
{ {
if (IsManager(user) || if (await organizationAccessService.HasInheritedWorkspacePermissionAsync(
await organizationAccessService.HasInheritedWorkspacePermissionAsync(
user, user,
workspaceId, workspaceId,
OrganizationPermissions.AccessOwnedWorkspaces, OrganizationPermissions.AccessOwnedWorkspaces,
@@ -128,8 +127,7 @@ internal sealed class AccessScopeService(
Guid campaignId, Guid campaignId,
CancellationToken ct) CancellationToken ct)
{ {
if (IsManager(user) || if (await organizationAccessService.HasInheritedWorkspacePermissionAsync(
await organizationAccessService.HasInheritedWorkspacePermissionAsync(
user, user,
workspaceId, workspaceId,
OrganizationPermissions.AccessOwnedWorkspaces, OrganizationPermissions.AccessOwnedWorkspaces,
@@ -149,7 +147,7 @@ internal sealed class AccessScopeService(
Guid campaignId, Guid campaignId,
CancellationToken ct) CancellationToken ct)
{ {
return IsManager(user) return IsManager(user) && await CanAccessCampaignAsync(user, workspaceId, clientId, campaignId, ct)
|| await organizationAccessService.HasInheritedWorkspacePermissionAsync( || await organizationAccessService.HasInheritedWorkspacePermissionAsync(
user, user,
workspaceId, workspaceId,
@@ -165,7 +163,7 @@ internal sealed class AccessScopeService(
Guid campaignId, Guid campaignId,
CancellationToken ct) CancellationToken ct)
{ {
return IsManager(user) return IsManager(user) && await CanAccessCampaignAsync(user, workspaceId, clientId, campaignId, ct)
|| await organizationAccessService.HasInheritedWorkspacePermissionAsync( || await organizationAccessService.HasInheritedWorkspacePermissionAsync(
user, user,
workspaceId, workspaceId,

View File

@@ -34,23 +34,20 @@ internal class GetCampaignsHandler(
{ {
IQueryable<Campaign> query = dbContext.Campaigns.AsQueryable(); IQueryable<Campaign> query = dbContext.Campaigns.AsQueryable();
if (!AccessScopeService.IsManager(User)) IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds();
IReadOnlyCollection<Guid> campaignScopeIds = User.GetCampaignScopeIds();
query = query.Where(campaign => workspaceScopeIds.Contains(campaign.WorkspaceId));
if (!AccessScopeService.IsManager(User) && clientScopeIds.Count > 0)
{ {
IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct); query = query.Where(campaign => clientScopeIds.Contains(campaign.ClientId));
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds(); }
IReadOnlyCollection<Guid> campaignScopeIds = User.GetCampaignScopeIds();
query = query.Where(campaign => workspaceScopeIds.Contains(campaign.WorkspaceId)); if (!AccessScopeService.IsManager(User) && campaignScopeIds.Count > 0)
{
if (clientScopeIds.Count > 0) query = query.Where(campaign => campaignScopeIds.Contains(campaign.Id));
{
query = query.Where(campaign => clientScopeIds.Contains(campaign.ClientId));
}
if (campaignScopeIds.Count > 0)
{
query = query.Where(campaign => campaignScopeIds.Contains(campaign.Id));
}
} }
if (request.ClientId.HasValue) if (request.ClientId.HasValue)

View File

@@ -23,11 +23,8 @@ internal class GetChannelsHandler(
{ {
IQueryable<Channel> query = dbContext.Channels.AsQueryable(); IQueryable<Channel> query = dbContext.Channels.AsQueryable();
if (!AccessScopeService.IsManager(User)) IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
{ query = query.Where(channel => workspaceScopeIds.Contains(channel.WorkspaceId));
IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
query = query.Where(channel => workspaceScopeIds.Contains(channel.WorkspaceId));
}
if (request.WorkspaceId.HasValue) if (request.WorkspaceId.HasValue)
{ {

View File

@@ -33,18 +33,14 @@ internal class GetClientsHandler(
{ {
IQueryable<Client> query = dbContext.Clients.AsQueryable(); IQueryable<Client> query = dbContext.Clients.AsQueryable();
if (!AccessScopeService.IsManager(User)) IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds();
query = query.Where(client => workspaceScopeIds.Contains(client.WorkspaceId));
if (!AccessScopeService.IsManager(User) && clientScopeIds.Count > 0)
{ {
IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct); query = query.Where(client => clientScopeIds.Contains(client.Id));
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds();
query = query.Where(client => workspaceScopeIds.Contains(client.WorkspaceId));
if (clientScopeIds.Count > 0)
{
query = query.Where(client => clientScopeIds.Contains(client.Id));
}
} }
if (request.WorkspaceId.HasValue) if (request.WorkspaceId.HasValue)

View File

@@ -37,23 +37,20 @@ internal class GetContentItemsHandler(
{ {
IQueryable<ContentItem> query = dbContext.ContentItems.AsQueryable(); IQueryable<ContentItem> query = dbContext.ContentItems.AsQueryable();
if (!AccessScopeService.IsManager(User)) IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds();
IReadOnlyCollection<Guid> campaignScopeIds = User.GetCampaignScopeIds();
query = query.Where(item => workspaceScopeIds.Contains(item.WorkspaceId));
if (!AccessScopeService.IsManager(User) && clientScopeIds.Count > 0)
{ {
IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct); query = query.Where(item => clientScopeIds.Contains(item.ClientId));
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds(); }
IReadOnlyCollection<Guid> campaignScopeIds = User.GetCampaignScopeIds();
query = query.Where(item => workspaceScopeIds.Contains(item.WorkspaceId)); if (!AccessScopeService.IsManager(User) && campaignScopeIds.Count > 0)
{
if (clientScopeIds.Count > 0) query = query.Where(item => campaignScopeIds.Contains(item.CampaignId));
{
query = query.Where(item => clientScopeIds.Contains(item.ClientId));
}
if (campaignScopeIds.Count > 0)
{
query = query.Where(item => campaignScopeIds.Contains(item.CampaignId));
}
} }
if (request.WorkspaceId.HasValue) if (request.WorkspaceId.HasValue)

View File

@@ -56,13 +56,10 @@ internal class GetNotificationsHandler(
IQueryable<NotificationEvent> query = dbContext.NotificationEvents.AsQueryable(); IQueryable<NotificationEvent> query = dbContext.NotificationEvents.AsQueryable();
Guid currentUserId = User.GetUserId(); Guid currentUserId = User.GetUserId();
if (!AccessScopeService.IsManager(User)) IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
{ query = query.Where(notificationEvent =>
IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct); workspaceScopeIds.Contains(notificationEvent.WorkspaceId) ||
query = query.Where(notificationEvent => notificationEvent.RecipientUserId == currentUserId);
workspaceScopeIds.Contains(notificationEvent.WorkspaceId) ||
notificationEvent.RecipientUserId == currentUserId);
}
query = query.Where(notificationEvent => query = query.Where(notificationEvent =>
notificationEvent.RecipientUserId == null || notificationEvent.RecipientUserId == null ||

View File

@@ -2,29 +2,16 @@ using System.Security.Claims;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Socialize.Api.Data; using Socialize.Api.Data;
using Socialize.Api.Infrastructure.Security; using Socialize.Api.Infrastructure.Security;
using Socialize.Api.Modules.Identity.Contracts;
namespace Socialize.Api.Modules.Organizations.Services; namespace Socialize.Api.Modules.Organizations.Services;
internal sealed class OrganizationAccessService( internal sealed class OrganizationAccessService(
AppDbContext dbContext) AppDbContext dbContext)
{ {
public static bool IsGlobalManager(ClaimsPrincipal user)
{
return user.IsInRole(KnownRoles.Administrator) || user.IsInRole(KnownRoles.Manager);
}
public async Task<IReadOnlyCollection<Guid>> GetAccessibleOrganizationIdsAsync( public async Task<IReadOnlyCollection<Guid>> GetAccessibleOrganizationIdsAsync(
ClaimsPrincipal user, ClaimsPrincipal user,
CancellationToken ct) CancellationToken ct)
{ {
if (IsGlobalManager(user))
{
return await dbContext.Organizations
.Select(organization => organization.Id)
.ToArrayAsync(ct);
}
Guid userId = user.GetUserId(); Guid userId = user.GetUserId();
Guid[] ownedOrganizationIds = await dbContext.Organizations Guid[] ownedOrganizationIds = await dbContext.Organizations
@@ -47,13 +34,6 @@ internal sealed class OrganizationAccessService(
ClaimsPrincipal user, ClaimsPrincipal user,
CancellationToken ct) CancellationToken ct)
{ {
if (IsGlobalManager(user))
{
return await dbContext.Workspaces
.Select(workspace => workspace.Id)
.ToArrayAsync(ct);
}
Guid[] directWorkspaceIds = user.GetWorkspaceScopeIds().ToArray(); Guid[] directWorkspaceIds = user.GetWorkspaceScopeIds().ToArray();
Guid[] organizationWorkspaceIds = await GetInheritedWorkspaceIdsAsync(user, OrganizationPermissions.AccessOwnedWorkspaces, ct); Guid[] organizationWorkspaceIds = await GetInheritedWorkspaceIdsAsync(user, OrganizationPermissions.AccessOwnedWorkspaces, ct);
@@ -68,11 +48,6 @@ internal sealed class OrganizationAccessService(
Guid organizationId, Guid organizationId,
CancellationToken ct) CancellationToken ct)
{ {
if (IsGlobalManager(user))
{
return true;
}
Guid userId = user.GetUserId(); Guid userId = user.GetUserId();
return await dbContext.Organizations.AnyAsync( return await dbContext.Organizations.AnyAsync(
@@ -89,11 +64,6 @@ internal sealed class OrganizationAccessService(
string permission, string permission,
CancellationToken ct) CancellationToken ct)
{ {
if (IsGlobalManager(user))
{
return true;
}
Guid userId = user.GetUserId(); Guid userId = user.GetUserId();
bool owner = await dbContext.Organizations.AnyAsync( bool owner = await dbContext.Organizations.AnyAsync(
@@ -117,11 +87,6 @@ internal sealed class OrganizationAccessService(
Guid organizationId, Guid organizationId,
CancellationToken ct) CancellationToken ct)
{ {
if (IsGlobalManager(user))
{
return OrganizationPermissionRules.GetPermissionsForRole(OrganizationRoles.Owner);
}
Guid userId = user.GetUserId(); Guid userId = user.GetUserId();
bool owner = await dbContext.Organizations.AnyAsync( bool owner = await dbContext.Organizations.AnyAsync(
@@ -150,11 +115,6 @@ internal sealed class OrganizationAccessService(
string permission, string permission,
CancellationToken ct) CancellationToken ct)
{ {
if (IsGlobalManager(user))
{
return true;
}
Guid? organizationId = await dbContext.Workspaces Guid? organizationId = await dbContext.Workspaces
.Where(workspace => workspace.Id == workspaceId) .Where(workspace => workspace.Id == workspaceId)
.Select(workspace => (Guid?)workspace.OrganizationId) .Select(workspace => (Guid?)workspace.OrganizationId)

View File

@@ -0,0 +1,49 @@
using System.Security.Claims;
using Socialize.Api.Infrastructure.Security;
using Socialize.Api.Modules.Identity.Contracts;
namespace Socialize.Tests.Security;
public class AccessScopeServiceTests
{
[Fact]
public void Manager_role_does_not_grant_workspace_access_without_workspace_scope()
{
Guid workspaceId = Guid.NewGuid();
ClaimsPrincipal user = CreateUser(KnownRoles.Manager);
Assert.False(AccessScopeService.CanAccessWorkspace(user, workspaceId));
Assert.False(AccessScopeService.CanManageWorkspace(user, workspaceId));
}
[Fact]
public void Administrator_role_does_not_grant_workspace_access_without_workspace_scope()
{
Guid workspaceId = Guid.NewGuid();
ClaimsPrincipal user = CreateUser(KnownRoles.Administrator);
Assert.False(AccessScopeService.CanAccessWorkspace(user, workspaceId));
Assert.False(AccessScopeService.CanManageWorkspace(user, workspaceId));
}
[Fact]
public void Manager_can_manage_only_workspaces_in_scope()
{
Guid workspaceId = Guid.NewGuid();
ClaimsPrincipal user = CreateUser(KnownRoles.Manager, new Claim(KnownClaims.WorkspaceScope, workspaceId.ToString()));
Assert.True(AccessScopeService.CanAccessWorkspace(user, workspaceId));
Assert.True(AccessScopeService.CanManageWorkspace(user, workspaceId));
}
private static ClaimsPrincipal CreateUser(string role, params Claim[] claims)
{
Claim[] baseClaims =
[
new(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString()),
new(ClaimTypes.Role, role),
];
return new ClaimsPrincipal(new ClaimsIdentity(baseClaims.Concat(claims), "Test"));
}
}