Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[AC-1387] Resource-based authentication for Groups #2845

Closed
wants to merge 53 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
73219ea
Initial proof of concept for resource based auth
eliykat Apr 10, 2023
09815d6
Use generic CRUD operations, remove unnecessary policies
eliykat Apr 10, 2023
313a5ae
Remove unused deps
eliykat Apr 10, 2023
22db106
Fill out the rest of GroupsController
eliykat Apr 11, 2023
ad3edfb
Move CreateGroup to Groups auth handler
eliykat Apr 11, 2023
1ce0c89
Add proper 403 exception and status code
eliykat Apr 11, 2023
4897086
Enable nullable where required
eliykat Apr 12, 2023
1da60bb
Use Guids instead of strings
eliykat Apr 12, 2023
c6c5a31
Unabbreviate Authorization
eliykat Apr 12, 2023
68ed862
Use nameof instead of string literals
eliykat Apr 12, 2023
7ae2162
Use named tuples
eliykat Apr 12, 2023
7fa4d15
Fix EF return tuple
eliykat Apr 12, 2023
22fa5b8
Assign default Permissions object, remove null checks
eliykat Apr 12, 2023
33a7a6d
Fix structure and async stuff in handlers
eliykat Apr 12, 2023
bde9519
Merge branch 'master' into experiment/resource-based-auth
eliykat Apr 24, 2023
1936bbe
Use switch statements
eliykat Apr 25, 2023
a2ed58b
Revert "Add proper 403 exception and status code"
eliykat Apr 25, 2023
9fba463
Check for null within auth handler
eliykat Apr 25, 2023
639b07b
Create extension methods for AuthService
eliykat Apr 25, 2023
933af46
Treat GroupUsers as their own resource
eliykat Apr 25, 2023
fee6057
Create shim methods for individual requirements
eliykat Apr 25, 2023
f4c7324
Fix line breaks
eliykat Apr 25, 2023
e1468c2
Remove unnecessary comment
eliykat Apr 25, 2023
50399e8
Add extra null checks
eliykat Apr 25, 2023
af23ee5
Remove GroupService
eliykat Apr 26, 2023
29a6d23
Add bitAuthorizationService and update tests
eliykat Apr 26, 2023
bf77e5a
Suggestion: move ReadAllGroups into group authorizationHandler
eliykat Apr 27, 2023
e577c24
Revert "Suggestion: move ReadAllGroups into group authorizationHandler"
eliykat Apr 27, 2023
2c06a80
Merge branch 'master' into experiment/resource-based-auth
eliykat May 9, 2023
f3681e2
Add provider checks using CurrentContext
eliykat May 9, 2023
e5f971e
Move ReadAllGroups requirement into group handler
eliykat May 10, 2023
3c52440
Fix sql syntax and update date
eliykat May 10, 2023
d73c81e
Fix comments and spacing
eliykat May 10, 2023
1cc18d4
Fix sql syntax
eliykat May 10, 2023
7f70cd3
Run dotnet format
eliykat May 10, 2023
27ff510
Delete unused code
eliykat May 10, 2023
4855003
Fix tests
eliykat May 10, 2023
6024216
Minor tweaks
eliykat May 10, 2023
2599427
Remove DeleteGroupCommand
eliykat May 11, 2023
89c65b9
Fix error message
eliykat May 11, 2023
3464d57
Remove #nullable from CurrentContext
eliykat May 11, 2023
719fcba
Merge branch 'master' into experiment/resource-based-auth
eliykat May 23, 2023
4465e1a
Fix order of using/namespace
eliykat May 23, 2023
f62a4f5
Make ReadAllForOrganization a static method
eliykat May 23, 2023
8413679
Merge remote-tracking branch 'origin/master' into experiment/resource…
eliykat May 29, 2023
6bba9f3
Remove access modifier in interface
eliykat May 29, 2023
c5583ab
Update date on sproc
eliykat May 29, 2023
eeb78a9
Use PascalCase for named tuple
eliykat May 29, 2023
5753267
Add comment
eliykat May 29, 2023
0ee583d
Finish updating named tuple case
eliykat May 30, 2023
b7b8053
Finish updating named tuple case
eliykat May 30, 2023
d5b68d8
Merge remote-tracking branch 'origin/master' into experiment/resource…
eliykat Aug 27, 2023
23622ee
dotnet format
eliykat Aug 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 73 additions & 18 deletions src/Api/Controllers/GroupsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Bit.Api.Models.Response;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.AuthorizationHandlers;
using Bit.Core.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
Expand All @@ -21,6 +22,7 @@ public class GroupsController : Controller
private readonly ICurrentContext _currentContext;
private readonly ICreateGroupCommand _createGroupCommand;
private readonly IUpdateGroupCommand _updateGroupCommand;
private readonly IAuthorizationService _authorizationService;

public GroupsController(
IGroupRepository groupRepository,
Expand All @@ -29,7 +31,8 @@ public GroupsController(
ICurrentContext currentContext,
ICreateGroupCommand createGroupCommand,
IUpdateGroupCommand updateGroupCommand,
IDeleteGroupCommand deleteGroupCommand)
IDeleteGroupCommand deleteGroupCommand,
IAuthorizationService authorizationService)
{
_groupRepository = groupRepository;
_groupService = groupService;
Expand All @@ -38,13 +41,20 @@ public GroupsController(
_createGroupCommand = createGroupCommand;
_updateGroupCommand = updateGroupCommand;
_deleteGroupCommand = deleteGroupCommand;
_authorizationService = authorizationService;
}

[HttpGet("{id}")]
public async Task<GroupResponseModel> Get(string orgId, string id)
eliykat marked this conversation as resolved.
Show resolved Hide resolved
{
var group = await _groupRepository.GetByIdAsync(new Guid(id));
if (group == null || !await _currentContext.ManageGroups(group.OrganizationId))
if (group == null)
{
throw new NotFoundException();
}

var authorizationResult = await _authorizationService.AuthorizeAsync(User, group, GroupOperations.Read);
eliykat marked this conversation as resolved.
Show resolved Hide resolved
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
Expand All @@ -56,7 +66,14 @@ public async Task<GroupResponseModel> Get(string orgId, string id)
public async Task<GroupDetailsResponseModel> GetDetails(string orgId, string id)
eliykat marked this conversation as resolved.
Show resolved Hide resolved
{
var groupDetails = await _groupRepository.GetByIdWithCollectionsAsync(new Guid(id));
if (groupDetails?.Item1 == null || !await _currentContext.ManageGroups(groupDetails.Item1.OrganizationId))
eliykat marked this conversation as resolved.
Show resolved Hide resolved
if (groupDetails?.Item1 == null)
{
throw new NotFoundException();
}

var authorizationResult =
eliykat marked this conversation as resolved.
Show resolved Hide resolved
await _authorizationService.AuthorizeAsync(User, groupDetails.Item1, GroupOperations.Read);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
Expand All @@ -67,17 +84,16 @@ public async Task<GroupDetailsResponseModel> GetDetails(string orgId, string id)
[HttpGet("")]
public async Task<ListResponseModel<GroupDetailsResponseModel>> Get(string orgId)
eliykat marked this conversation as resolved.
Show resolved Hide resolved
{
var orgIdGuid = new Guid(orgId);
var canAccess = await _currentContext.ManageGroups(orgIdGuid) ||
await _currentContext.ViewAssignedCollections(orgIdGuid) ||
await _currentContext.ViewAllCollections(orgIdGuid) ||
await _currentContext.ManageUsers(orgIdGuid);
var org = _currentContext.GetOrganization(orgId);

if (!canAccess)
var authorizationResult =
await _authorizationService.AuthorizeAsync(User, org, OrganizationOperations.ReadAllGroups);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}

var orgIdGuid = new Guid(orgId);
var groups = await _groupRepository.GetManyWithCollectionsByOrganizationIdAsync(orgIdGuid);
var responses = groups.Select(g => new GroupDetailsResponseModel(g.Item1, g.Item2));
return new ListResponseModel<GroupDetailsResponseModel>(responses);
Expand All @@ -88,7 +104,14 @@ public async Task<IEnumerable<Guid>> GetUsers(string orgId, string id)
{
var idGuid = new Guid(id);
var group = await _groupRepository.GetByIdAsync(idGuid);
if (group == null || !await _currentContext.ManageGroups(group.OrganizationId))
if (group == null)
{
throw new NotFoundException();
}

var authorizationResult =
eliykat marked this conversation as resolved.
Show resolved Hide resolved
await _authorizationService.AuthorizeAsync(User, group, GroupOperations.Read);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
Expand All @@ -101,13 +124,16 @@ public async Task<IEnumerable<Guid>> GetUsers(string orgId, string id)
public async Task<GroupResponseModel> Post(string orgId, [FromBody] GroupRequestModel model)
{
var orgIdGuid = new Guid(orgId);
if (!await _currentContext.ManageGroups(orgIdGuid))
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
var group = model.ToGroup(orgIdGuid);

var authorizationResult =
await _authorizationService.AuthorizeAsync(User, group, GroupOperations.Create);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}

var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
var group = model.ToGroup(orgIdGuid);
await _createGroupCommand.CreateGroupAsync(group, organization, model.Collections?.Select(c => c.ToSelectionReadOnly()), model.Users);

return new GroupResponseModel(group);
Expand All @@ -118,7 +144,14 @@ public async Task<GroupResponseModel> Post(string orgId, [FromBody] GroupRequest
public async Task<GroupResponseModel> Put(string orgId, string id, [FromBody] GroupRequestModel model)
{
var group = await _groupRepository.GetByIdAsync(new Guid(id));
if (group == null || !await _currentContext.ManageGroups(group.OrganizationId))
if (group == null)
{
throw new NotFoundException();
}

var authorizationResult =
await _authorizationService.AuthorizeAsync(User, group, GroupOperations.Update);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
Expand All @@ -134,10 +167,18 @@ public async Task<GroupResponseModel> Put(string orgId, string id, [FromBody] Gr
public async Task PutUsers(string orgId, string id, [FromBody] IEnumerable<Guid> model)
{
var group = await _groupRepository.GetByIdAsync(new Guid(id));
if (group == null || !await _currentContext.ManageGroups(group.OrganizationId))
if (group == null)
{
throw new NotFoundException();
}

var authorizationResult =
await _authorizationService.AuthorizeAsync(User, group, GroupOperations.AddUser);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}

await _groupRepository.UpdateUsersAsync(group.Id, model);
}

Expand All @@ -146,7 +187,14 @@ public async Task PutUsers(string orgId, string id, [FromBody] IEnumerable<Guid>
public async Task Delete(string orgId, string id)
{
var group = await _groupRepository.GetByIdAsync(new Guid(id));
if (group == null || !await _currentContext.ManageGroups(group.OrganizationId))
if (group == null)
{
throw new NotFoundException();
}

var authorizationResult =
await _authorizationService.AuthorizeAsync(User, group, GroupOperations.Delete);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
Expand All @@ -162,7 +210,8 @@ public async Task BulkDelete([FromBody] GroupBulkRequestModel model)

foreach (var group in groups)
{
if (!await _currentContext.ManageGroups(group.OrganizationId))
var authorizationResult = await _authorizationService.AuthorizeAsync(User, group, GroupOperations.Delete);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
Expand All @@ -176,7 +225,13 @@ public async Task BulkDelete([FromBody] GroupBulkRequestModel model)
public async Task Delete(string orgId, string id, string orgUserId)
{
var group = await _groupRepository.GetByIdAsync(new Guid(id));
if (group == null || !await _currentContext.ManageGroups(group.OrganizationId))
if (group == null)
{
throw new NotFoundException();
}

var authorizationResult = await _authorizationService.AuthorizeAsync(User, group, GroupOperations.DeleteUser);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
Expand Down
11 changes: 11 additions & 0 deletions src/Core/Context/CurrentContext.cs
eliykat marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -529,4 +529,15 @@ protected async Task<IEnumerable<ProviderUserOrganizationDetails>> GetProviderOr

return _providerUserOrganizations;
}

public CurrentContentOrganization? GetOrganization(string orgId)
{
var guid = new Guid(orgId);
return GetOrganization(guid);
}
eliykat marked this conversation as resolved.
Show resolved Hide resolved

public CurrentContentOrganization? GetOrganization(Guid orgId)
{
return Organizations.Find(o => o.Id == orgId);
}
}
2 changes: 2 additions & 0 deletions src/Core/Context/ICurrentContext.cs
eliykat marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,6 @@ Task<ICollection<CurrentContentProvider>> ProviderMembershipAsync(

Task<Guid?> ProviderIdForOrg(Guid orgId);
bool AccessSecretsManager(Guid organizationId);
public CurrentContentOrganization? GetOrganization(string orgId);
public CurrentContentOrganization? GetOrganization(Guid orgId);
eliykat marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Microsoft.AspNetCore.Authorization;

namespace Bit.Core.OrganizationFeatures.AuthorizationHandlers;

class GroupAuthHandler : AuthorizationHandler<GroupOperationRequirement, Group>
eliykat marked this conversation as resolved.
Show resolved Hide resolved
{
private readonly ICurrentContext _currentContext;

public GroupAuthHandler(ICurrentContext currentContext)
{
_currentContext = currentContext;
}

protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
eliykat marked this conversation as resolved.
Show resolved Hide resolved
GroupOperationRequirement requirement,
Group resource)
{
eliykat marked this conversation as resolved.
Show resolved Hide resolved
// Currently all GroupOperationRequirements have the same permission requirements
// TODO: providers need to be included in the claims
var org = _currentContext.GetOrganization(resource.OrganizationId);
var canAccess = org.Type == OrganizationUserType.Owner ||
org.Type == OrganizationUserType.Admin ||
(org.Permissions?.ManageGroups ?? false);

if (canAccess)
{
context.Succeed(requirement);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace Bit.Core.OrganizationFeatures.AuthorizationHandlers;

public class GroupOperationRequirement : OperationAuthorizationRequirement { }

public static class GroupOperations
{
// Operations on the Group object itself
public static readonly GroupOperationRequirement Create = new() { Name = "Create" };
eliykat marked this conversation as resolved.
Show resolved Hide resolved
public static readonly GroupOperationRequirement Read = new() { Name = "Read" };
public static readonly GroupOperationRequirement Update = new() { Name = "Update" };
public static readonly GroupOperationRequirement Delete = new() { Name = "Delete" };

// Operations on Group-User associations
public static readonly GroupOperationRequirement AddUser = new() { Name = "AddUser" };
public static readonly GroupOperationRequirement DeleteUser = new() { Name = "DeleteUser" };
eliykat marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Bit.Core.Context;
using Bit.Core.Enums;
using Microsoft.AspNetCore.Authorization;

namespace Bit.Core.OrganizationFeatures.AuthorizationHandlers;

class OrganizationAuthHandler : AuthorizationHandler<OrganizationOperationRequirement, CurrentContentOrganization>
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
OrganizationOperationRequirement requirement,
CurrentContentOrganization resource)
{
if (requirement == OrganizationOperations.ReadAllGroups)
{
await ReadAllGroupsAsync(context, requirement, resource);
return;
}
}

private async Task ReadAllGroupsAsync(AuthorizationHandlerContext context,
OrganizationOperationRequirement requirement,
CurrentContentOrganization resource)
{
// TODO: providers need to be included in the claims

var canAccess = resource.Type == OrganizationUserType.Owner ||
resource.Type == OrganizationUserType.Admin ||
resource.Type == OrganizationUserType.Manager ||
(resource.Permissions?.ManageGroups ?? false) ||
(resource.Permissions?.EditAssignedCollections ?? false) ||
(resource.Permissions?.DeleteAssignedCollections ?? false) ||
(resource.Permissions?.CreateNewCollections ?? false) ||
(resource.Permissions?.EditAnyCollection ?? false) ||
(resource.Permissions?.DeleteAnyCollection ?? false);
eliykat marked this conversation as resolved.
Show resolved Hide resolved

if (canAccess)
{
context.Succeed(requirement);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace Bit.Core.OrganizationFeatures.AuthorizationHandlers;

public class OrganizationOperationRequirement : OperationAuthorizationRequirement { }

public static class OrganizationOperations
{
public static readonly OrganizationOperationRequirement ReadAllGroups = new() { Name = "ReadAllGroups" };
eliykat marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Bit.Core.Models.Business.Tokenables;
using Bit.Core.OrganizationFeatures.AuthorizationHandlers;
using Bit.Core.OrganizationFeatures.Groups;
using Bit.Core.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationApiKeys;
Expand All @@ -18,6 +19,7 @@
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tokens;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
Expand All @@ -30,6 +32,7 @@ public static void AddOrganizationServices(this IServiceCollection services, IGl
{
services.AddScoped<IOrganizationService, OrganizationService>();
services.AddTokenizers();
services.AddAuthorizationHandlers();
services.AddOrganizationGroupCommands();
services.AddOrganizationConnectionCommands();
services.AddOrganizationSponsorshipCommands(globalSettings);
Expand Down Expand Up @@ -117,4 +120,10 @@ private static void AddTokenizers(this IServiceCollection services)
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable>>>())
);
}

private static void AddAuthorizationHandlers(this IServiceCollection services)
{
services.AddScoped<IAuthorizationHandler, OrganizationAuthHandler>();
services.AddScoped<IAuthorizationHandler, GroupAuthHandler>();
}
}