diff --git a/backend/src/Socialize.Api/Infrastructure/Security/AccessScopeService.cs b/backend/src/Socialize.Api/Infrastructure/Security/AccessScopeService.cs index 6620be58..2fbb76c1 100644 --- a/backend/src/Socialize.Api/Infrastructure/Security/AccessScopeService.cs +++ b/backend/src/Socialize.Api/Infrastructure/Security/AccessScopeService.cs @@ -24,7 +24,7 @@ internal sealed class AccessScopeService( 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) @@ -34,24 +34,25 @@ internal sealed class AccessScopeService( public static bool CanAccessClient(ClaimsPrincipal user, Guid workspaceId, Guid clientId) { - return IsManager(user) - || (CanAccessWorkspace(user, workspaceId) && user.GetClientScopeIds().Contains(clientId)); + return CanAccessWorkspace(user, workspaceId) && + (IsManager(user) || user.GetClientScopeIds().Contains(clientId)); } public static bool CanAccessCampaign(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId) { - return IsManager(user) - || (CanAccessClient(user, workspaceId, clientId) && user.GetCampaignScopeIds().Contains(campaignId)); + return CanAccessClient(user, workspaceId, clientId) && + (IsManager(user) || user.GetCampaignScopeIds().Contains(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) { - return IsManager(user) + return IsManager(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId) || IsProvider(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId) || IsClient(user) && CanAccessClient(user, workspaceId, clientId); } @@ -68,7 +69,7 @@ internal sealed class AccessScopeService( Guid workspaceId, CancellationToken ct) { - return CanAccessWorkspace(user, workspaceId) + return user.GetWorkspaceScopeIds().Contains(workspaceId) || await organizationAccessService.HasInheritedWorkspacePermissionAsync( user, workspaceId, @@ -81,7 +82,7 @@ internal sealed class AccessScopeService( Guid workspaceId, CancellationToken ct) { - return IsManager(user) + return CanManageWorkspace(user, workspaceId) || await organizationAccessService.HasInheritedWorkspacePermissionAsync( user, workspaceId, @@ -94,8 +95,7 @@ internal sealed class AccessScopeService( Guid organizationId, CancellationToken ct) { - return IsManager(user) - || await organizationAccessService.HasOrganizationPermissionAsync( + return await organizationAccessService.HasOrganizationPermissionAsync( user, organizationId, OrganizationPermissions.CreateWorkspaces, @@ -108,8 +108,7 @@ internal sealed class AccessScopeService( Guid clientId, CancellationToken ct) { - if (IsManager(user) || - await organizationAccessService.HasInheritedWorkspacePermissionAsync( + if (await organizationAccessService.HasInheritedWorkspacePermissionAsync( user, workspaceId, OrganizationPermissions.AccessOwnedWorkspaces, @@ -128,8 +127,7 @@ internal sealed class AccessScopeService( Guid campaignId, CancellationToken ct) { - if (IsManager(user) || - await organizationAccessService.HasInheritedWorkspacePermissionAsync( + if (await organizationAccessService.HasInheritedWorkspacePermissionAsync( user, workspaceId, OrganizationPermissions.AccessOwnedWorkspaces, @@ -149,7 +147,7 @@ internal sealed class AccessScopeService( Guid campaignId, CancellationToken ct) { - return IsManager(user) + return IsManager(user) && await CanAccessCampaignAsync(user, workspaceId, clientId, campaignId, ct) || await organizationAccessService.HasInheritedWorkspacePermissionAsync( user, workspaceId, @@ -165,7 +163,7 @@ internal sealed class AccessScopeService( Guid campaignId, CancellationToken ct) { - return IsManager(user) + return IsManager(user) && await CanAccessCampaignAsync(user, workspaceId, clientId, campaignId, ct) || await organizationAccessService.HasInheritedWorkspacePermissionAsync( user, workspaceId, diff --git a/backend/src/Socialize.Api/Modules/Campaigns/Handlers/GetCampaigns.cs b/backend/src/Socialize.Api/Modules/Campaigns/Handlers/GetCampaigns.cs index 9c05501b..be5c0037 100644 --- a/backend/src/Socialize.Api/Modules/Campaigns/Handlers/GetCampaigns.cs +++ b/backend/src/Socialize.Api/Modules/Campaigns/Handlers/GetCampaigns.cs @@ -34,23 +34,20 @@ internal class GetCampaignsHandler( { IQueryable query = dbContext.Campaigns.AsQueryable(); - if (!AccessScopeService.IsManager(User)) + IReadOnlyCollection workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct); + IReadOnlyCollection clientScopeIds = User.GetClientScopeIds(); + IReadOnlyCollection campaignScopeIds = User.GetCampaignScopeIds(); + + query = query.Where(campaign => workspaceScopeIds.Contains(campaign.WorkspaceId)); + + if (!AccessScopeService.IsManager(User) && clientScopeIds.Count > 0) { - IReadOnlyCollection workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct); - IReadOnlyCollection clientScopeIds = User.GetClientScopeIds(); - IReadOnlyCollection campaignScopeIds = User.GetCampaignScopeIds(); + query = query.Where(campaign => clientScopeIds.Contains(campaign.ClientId)); + } - query = query.Where(campaign => workspaceScopeIds.Contains(campaign.WorkspaceId)); - - if (clientScopeIds.Count > 0) - { - query = query.Where(campaign => clientScopeIds.Contains(campaign.ClientId)); - } - - if (campaignScopeIds.Count > 0) - { - query = query.Where(campaign => campaignScopeIds.Contains(campaign.Id)); - } + if (!AccessScopeService.IsManager(User) && campaignScopeIds.Count > 0) + { + query = query.Where(campaign => campaignScopeIds.Contains(campaign.Id)); } if (request.ClientId.HasValue) diff --git a/backend/src/Socialize.Api/Modules/Channels/Handlers/GetChannels.cs b/backend/src/Socialize.Api/Modules/Channels/Handlers/GetChannels.cs index 8c3fcfc8..c601bdc7 100644 --- a/backend/src/Socialize.Api/Modules/Channels/Handlers/GetChannels.cs +++ b/backend/src/Socialize.Api/Modules/Channels/Handlers/GetChannels.cs @@ -23,11 +23,8 @@ internal class GetChannelsHandler( { IQueryable query = dbContext.Channels.AsQueryable(); - if (!AccessScopeService.IsManager(User)) - { - IReadOnlyCollection workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct); - query = query.Where(channel => workspaceScopeIds.Contains(channel.WorkspaceId)); - } + IReadOnlyCollection workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct); + query = query.Where(channel => workspaceScopeIds.Contains(channel.WorkspaceId)); if (request.WorkspaceId.HasValue) { diff --git a/backend/src/Socialize.Api/Modules/Clients/Handlers/GetClients.cs b/backend/src/Socialize.Api/Modules/Clients/Handlers/GetClients.cs index 5911de55..e2e2003f 100644 --- a/backend/src/Socialize.Api/Modules/Clients/Handlers/GetClients.cs +++ b/backend/src/Socialize.Api/Modules/Clients/Handlers/GetClients.cs @@ -33,18 +33,14 @@ internal class GetClientsHandler( { IQueryable query = dbContext.Clients.AsQueryable(); - if (!AccessScopeService.IsManager(User)) + IReadOnlyCollection workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct); + IReadOnlyCollection clientScopeIds = User.GetClientScopeIds(); + + query = query.Where(client => workspaceScopeIds.Contains(client.WorkspaceId)); + + if (!AccessScopeService.IsManager(User) && clientScopeIds.Count > 0) { - IReadOnlyCollection workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct); - IReadOnlyCollection clientScopeIds = User.GetClientScopeIds(); - - query = query.Where(client => workspaceScopeIds.Contains(client.WorkspaceId)); - - if (clientScopeIds.Count > 0) - { - query = query.Where(client => clientScopeIds.Contains(client.Id)); - } - + query = query.Where(client => clientScopeIds.Contains(client.Id)); } if (request.WorkspaceId.HasValue) diff --git a/backend/src/Socialize.Api/Modules/ContentItems/Handlers/GetContentItems.cs b/backend/src/Socialize.Api/Modules/ContentItems/Handlers/GetContentItems.cs index 0e46b0c4..d4f729df 100644 --- a/backend/src/Socialize.Api/Modules/ContentItems/Handlers/GetContentItems.cs +++ b/backend/src/Socialize.Api/Modules/ContentItems/Handlers/GetContentItems.cs @@ -37,23 +37,20 @@ internal class GetContentItemsHandler( { IQueryable query = dbContext.ContentItems.AsQueryable(); - if (!AccessScopeService.IsManager(User)) + IReadOnlyCollection workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct); + IReadOnlyCollection clientScopeIds = User.GetClientScopeIds(); + IReadOnlyCollection campaignScopeIds = User.GetCampaignScopeIds(); + + query = query.Where(item => workspaceScopeIds.Contains(item.WorkspaceId)); + + if (!AccessScopeService.IsManager(User) && clientScopeIds.Count > 0) { - IReadOnlyCollection workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct); - IReadOnlyCollection clientScopeIds = User.GetClientScopeIds(); - IReadOnlyCollection campaignScopeIds = User.GetCampaignScopeIds(); + query = query.Where(item => clientScopeIds.Contains(item.ClientId)); + } - query = query.Where(item => workspaceScopeIds.Contains(item.WorkspaceId)); - - if (clientScopeIds.Count > 0) - { - query = query.Where(item => clientScopeIds.Contains(item.ClientId)); - } - - if (campaignScopeIds.Count > 0) - { - query = query.Where(item => campaignScopeIds.Contains(item.CampaignId)); - } + if (!AccessScopeService.IsManager(User) && campaignScopeIds.Count > 0) + { + query = query.Where(item => campaignScopeIds.Contains(item.CampaignId)); } if (request.WorkspaceId.HasValue) diff --git a/backend/src/Socialize.Api/Modules/Notifications/Handlers/GetNotifications.cs b/backend/src/Socialize.Api/Modules/Notifications/Handlers/GetNotifications.cs index 24ed66f5..5b0e184c 100644 --- a/backend/src/Socialize.Api/Modules/Notifications/Handlers/GetNotifications.cs +++ b/backend/src/Socialize.Api/Modules/Notifications/Handlers/GetNotifications.cs @@ -56,13 +56,10 @@ internal class GetNotificationsHandler( IQueryable query = dbContext.NotificationEvents.AsQueryable(); Guid currentUserId = User.GetUserId(); - if (!AccessScopeService.IsManager(User)) - { - IReadOnlyCollection workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct); - query = query.Where(notificationEvent => - workspaceScopeIds.Contains(notificationEvent.WorkspaceId) || - notificationEvent.RecipientUserId == currentUserId); - } + IReadOnlyCollection workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct); + query = query.Where(notificationEvent => + workspaceScopeIds.Contains(notificationEvent.WorkspaceId) || + notificationEvent.RecipientUserId == currentUserId); query = query.Where(notificationEvent => notificationEvent.RecipientUserId == null || diff --git a/backend/src/Socialize.Api/Modules/Organizations/Services/OrganizationAccessService.cs b/backend/src/Socialize.Api/Modules/Organizations/Services/OrganizationAccessService.cs index c3e8caeb..c2216837 100644 --- a/backend/src/Socialize.Api/Modules/Organizations/Services/OrganizationAccessService.cs +++ b/backend/src/Socialize.Api/Modules/Organizations/Services/OrganizationAccessService.cs @@ -2,29 +2,16 @@ using System.Security.Claims; using Microsoft.EntityFrameworkCore; using Socialize.Api.Data; using Socialize.Api.Infrastructure.Security; -using Socialize.Api.Modules.Identity.Contracts; namespace Socialize.Api.Modules.Organizations.Services; internal sealed class OrganizationAccessService( AppDbContext dbContext) { - public static bool IsGlobalManager(ClaimsPrincipal user) - { - return user.IsInRole(KnownRoles.Administrator) || user.IsInRole(KnownRoles.Manager); - } - public async Task> GetAccessibleOrganizationIdsAsync( ClaimsPrincipal user, CancellationToken ct) { - if (IsGlobalManager(user)) - { - return await dbContext.Organizations - .Select(organization => organization.Id) - .ToArrayAsync(ct); - } - Guid userId = user.GetUserId(); Guid[] ownedOrganizationIds = await dbContext.Organizations @@ -47,13 +34,6 @@ internal sealed class OrganizationAccessService( ClaimsPrincipal user, CancellationToken ct) { - if (IsGlobalManager(user)) - { - return await dbContext.Workspaces - .Select(workspace => workspace.Id) - .ToArrayAsync(ct); - } - Guid[] directWorkspaceIds = user.GetWorkspaceScopeIds().ToArray(); Guid[] organizationWorkspaceIds = await GetInheritedWorkspaceIdsAsync(user, OrganizationPermissions.AccessOwnedWorkspaces, ct); @@ -68,11 +48,6 @@ internal sealed class OrganizationAccessService( Guid organizationId, CancellationToken ct) { - if (IsGlobalManager(user)) - { - return true; - } - Guid userId = user.GetUserId(); return await dbContext.Organizations.AnyAsync( @@ -89,11 +64,6 @@ internal sealed class OrganizationAccessService( string permission, CancellationToken ct) { - if (IsGlobalManager(user)) - { - return true; - } - Guid userId = user.GetUserId(); bool owner = await dbContext.Organizations.AnyAsync( @@ -117,11 +87,6 @@ internal sealed class OrganizationAccessService( Guid organizationId, CancellationToken ct) { - if (IsGlobalManager(user)) - { - return OrganizationPermissionRules.GetPermissionsForRole(OrganizationRoles.Owner); - } - Guid userId = user.GetUserId(); bool owner = await dbContext.Organizations.AnyAsync( @@ -150,11 +115,6 @@ internal sealed class OrganizationAccessService( string permission, CancellationToken ct) { - if (IsGlobalManager(user)) - { - return true; - } - Guid? organizationId = await dbContext.Workspaces .Where(workspace => workspace.Id == workspaceId) .Select(workspace => (Guid?)workspace.OrganizationId) diff --git a/backend/tests/Socialize.Tests/Security/AccessScopeServiceTests.cs b/backend/tests/Socialize.Tests/Security/AccessScopeServiceTests.cs new file mode 100644 index 00000000..487bbf64 --- /dev/null +++ b/backend/tests/Socialize.Tests/Security/AccessScopeServiceTests.cs @@ -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")); + } +}