Skip to content

Commit

Permalink
[SM-193] Access Policy (#2359)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hinton authored Dec 9, 2022
1 parent 2cea7bd commit b3cf05e
Show file tree
Hide file tree
Showing 24 changed files with 5,334 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Bit.Commercial.Infrastructure.EntityFramework.Repositories;
using Bit.Core.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories;
using Microsoft.Extensions.DependencyInjection;

namespace Bit.Commercial.Infrastructure.EntityFramework;
Expand All @@ -8,6 +9,7 @@ public static class CommercialEFServiceCollectionExtensions
{
public static void AddCommercialEFRepositories(this IServiceCollection services)
{
services.AddSingleton<IAccessPolicyRepository, AccessPolicyRepository>();
services.AddSingleton<ISecretRepository, SecretRepository>();
services.AddSingleton<IProjectRepository, ProjectRepository>();
services.AddSingleton<IServiceAccountRepository, ServiceAccountRepository>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using AutoMapper;
using System.Linq.Expressions;
using AutoMapper;
using Bit.Core.Repositories;
using Bit.Infrastructure.EntityFramework.Models;
using Bit.Infrastructure.EntityFramework.Repositories;
Expand All @@ -25,19 +26,23 @@ public ProjectRepository(IServiceScopeFactory serviceScopeFactory, IMapper mappe
}
}

public async Task<IEnumerable<Core.Entities.Project>> GetManyByOrganizationIdAsync(Guid organizationId)
public async Task<IEnumerable<Core.Entities.Project>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var project = await dbContext.Project
.Where(c => c.OrganizationId == organizationId && c.DeletedDate == null)
.OrderBy(c => c.RevisionDate)
.ToListAsync();
return Mapper.Map<List<Core.Entities.Project>>(project);
}
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var project = await dbContext.Project
.Where(p => p.OrganizationId == organizationId && p.DeletedDate == null)
// TODO: Enable this + Handle Admins
//.Where(UserHasAccessToProject(userId))
.OrderBy(p => p.RevisionDate)
.ToListAsync();
return Mapper.Map<List<Core.Entities.Project>>(project);
}

private static Expression<Func<Project, bool>> UserHasAccessToProject(Guid userId) => p =>
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
p.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read));

public async Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
{
using (var scope = ServiceScopeFactory.CreateScope())
Expand Down
7 changes: 6 additions & 1 deletion src/Api/Controllers/ProjectsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,28 @@
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.SecretManagerFeatures.Projects.Interfaces;
using Bit.Core.Services;
using Microsoft.AspNetCore.Mvc;

namespace Bit.Api.Controllers;

[SecretsManager]
public class ProjectsController : Controller
{
private readonly IUserService _userService;
private readonly IProjectRepository _projectRepository;
private readonly ICreateProjectCommand _createProjectCommand;
private readonly IUpdateProjectCommand _updateProjectCommand;
private readonly IDeleteProjectCommand _deleteProjectCommand;

public ProjectsController(
IUserService userService,
IProjectRepository projectRepository,
ICreateProjectCommand createProjectCommand,
IUpdateProjectCommand updateProjectCommand,
IDeleteProjectCommand deleteProjectCommand)
{
_userService = userService;
_projectRepository = projectRepository;
_createProjectCommand = createProjectCommand;
_updateProjectCommand = updateProjectCommand;
Expand All @@ -46,7 +50,8 @@ public async Task<ProjectResponseModel> UpdateProjectAsync([FromRoute] Guid id,
[HttpGet("organizations/{organizationId}/projects")]
public async Task<ListResponseModel<ProjectResponseModel>> GetProjectsByOrganizationAsync([FromRoute] Guid organizationId)
{
var projects = await _projectRepository.GetManyByOrganizationIdAsync(organizationId);
var userId = _userService.GetProperUserId(User).Value;
var projects = await _projectRepository.GetManyByOrganizationIdAsync(organizationId, userId);
var responses = projects.Select(project => new ProjectResponseModel(project));
return new ListResponseModel<ProjectResponseModel>(responses);
}
Expand Down
76 changes: 76 additions & 0 deletions src/Core/Entities/AccessPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using Bit.Core.Utilities;

namespace Bit.Core.Entities;

public class AccessPolicy : ITableObject<Guid>
{
public Guid Id { get; set; }

// Object to grant access from
public Guid? OrganizationUserId { get; set; }
public Guid? GroupId { get; set; }
public Guid? ServiceAccountId { get; set; }

// Object to grant access to
public Guid? GrantedProjectId { get; set; }
public Guid? GrantedServiceAccountId { get; set; }

// Access
public bool Read { get; set; }
public bool Write { get; set; }

public DateTime CreationDate { get; set; }
public DateTime RevisionDate { get; set; }

public void SetNewId()
{
Id = CoreHelpers.GenerateComb();
}
}

public abstract class BaseAccessPolicy
{
public Guid Id { get; set; }

// Access
public bool Read { get; set; }
public bool Write { get; set; }

public DateTime CreationDate { get; set; }
public DateTime RevisionDate { get; set; }

public void SetNewId()
{
Id = CoreHelpers.GenerateComb();
}
}

public class UserProjectAccessPolicy : BaseAccessPolicy
{
public Guid? OrganizationUserId { get; set; }
public Guid? GrantedProjectId { get; set; }
}

public class UserServiceAccountAccessPolicy : BaseAccessPolicy
{
public Guid? OrganizationUserId { get; set; }
public Guid? GrantedServiceAccountId { get; set; }
}

public class GroupProjectAccessPolicy : BaseAccessPolicy
{
public Guid? GroupId { get; set; }
public Guid? GrantedProjectId { get; set; }
}

public class GroupServiceAccountAccessPolicy : BaseAccessPolicy
{
public Guid? GroupId { get; set; }
public Guid? GrantedServiceAccountId { get; set; }
}

public class ServiceAccountProjectAccessPolicy : BaseAccessPolicy
{
public Guid? ServiceAccountId { get; set; }
public Guid? GrantedProjectId { get; set; }
}
7 changes: 7 additions & 0 deletions src/Core/Repositories/IAccessPolicyRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using Bit.Core.Entities;

namespace Bit.Core.Repositories;

public interface IAccessPolicyRepository : IRepository<AccessPolicy, Guid>
{
}
2 changes: 1 addition & 1 deletion src/Core/Repositories/IProjectRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace Bit.Core.Repositories;

public interface IProjectRepository
{
Task<IEnumerable<Project>> GetManyByOrganizationIdAsync(Guid organizationId);
Task<IEnumerable<Project>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId);
Task<IEnumerable<Project>> GetManyByIds(IEnumerable<Guid> ids);
Task<Project> GetByIdAsync(Guid id);
Task<Project> CreateAsync(Project project);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using Bit.Infrastructure.EntityFramework.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Bit.Infrastructure.EntityFramework.Configurations;

public class AccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration<AccessPolicy>
{
public void Configure(EntityTypeBuilder<AccessPolicy> builder)
{
builder
.HasDiscriminator<string>("Discriminator")
.HasValue<UserProjectAccessPolicy>("user_project")
.HasValue<UserServiceAccountAccessPolicy>("user_service_account")
.HasValue<GroupProjectAccessPolicy>("group_project")
.HasValue<GroupServiceAccountAccessPolicy>("group_service_account")
.HasValue<ServiceAccountProjectAccessPolicy>("service_account_project");

builder
.Property(s => s.Id)
.ValueGeneratedNever();

builder
.HasKey(s => s.Id)
.IsClustered();

builder.ToTable(nameof(AccessPolicy));
}
}

public class UserProjectAccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration<UserProjectAccessPolicy>
{
public void Configure(EntityTypeBuilder<UserProjectAccessPolicy> builder)
{
builder
.Property(e => e.OrganizationUserId)
.HasColumnName(nameof(UserProjectAccessPolicy.OrganizationUserId));

builder
.Property(e => e.GrantedProjectId)
.HasColumnName(nameof(UserProjectAccessPolicy.GrantedProjectId));
}
}

public class UserServiceAccountAccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration<UserServiceAccountAccessPolicy>
{
public void Configure(EntityTypeBuilder<UserServiceAccountAccessPolicy> builder)
{
builder
.Property(e => e.OrganizationUserId)
.HasColumnName(nameof(UserServiceAccountAccessPolicy.OrganizationUserId));

builder
.Property(e => e.GrantedServiceAccountId)
.HasColumnName(nameof(UserServiceAccountAccessPolicy.GrantedServiceAccountId));
}
}

public class GroupProjectAccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration<GroupProjectAccessPolicy>
{
public void Configure(EntityTypeBuilder<GroupProjectAccessPolicy> builder)
{
builder
.Property(e => e.GroupId)
.HasColumnName(nameof(GroupProjectAccessPolicy.GroupId));

builder
.Property(e => e.GrantedProjectId)
.HasColumnName(nameof(GroupProjectAccessPolicy.GrantedProjectId));
}
}

public class GroupServiceAccountAccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration<GroupServiceAccountAccessPolicy>
{
public void Configure(EntityTypeBuilder<GroupServiceAccountAccessPolicy> builder)
{
builder
.Property(e => e.GroupId)
.HasColumnName(nameof(GroupServiceAccountAccessPolicy.GroupId));

builder
.Property(e => e.GrantedServiceAccountId)
.HasColumnName(nameof(GroupServiceAccountAccessPolicy.GrantedServiceAccountId));
}
}

public class ServiceAccountProjectAccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration<ServiceAccountProjectAccessPolicy>
{
public void Configure(EntityTypeBuilder<ServiceAccountProjectAccessPolicy> builder)
{
builder
.Property(e => e.ServiceAccountId)
.HasColumnName(nameof(ServiceAccountProjectAccessPolicy.ServiceAccountId));

builder
.Property(e => e.GrantedProjectId)
.HasColumnName(nameof(ServiceAccountProjectAccessPolicy.GrantedProjectId));
}
}
60 changes: 60 additions & 0 deletions src/Infrastructure.EntityFramework/Models/AccessPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using AutoMapper;

namespace Bit.Infrastructure.EntityFramework.Models;

public class BaseAccessPolicy : Core.Entities.BaseAccessPolicy
{
public string Discriminator { get; set; }
}

public class AccessPolicyMapperProfile : Profile
{
public AccessPolicyMapperProfile()
{
CreateMap<Core.Entities.AccessPolicy, AccessPolicy>().ReverseMap();
}
}

public class AccessPolicy : BaseAccessPolicy
{
}

public class UserProjectAccessPolicy : AccessPolicy
{
public Guid? OrganizationUserId { get; set; }
public virtual OrganizationUser OrganizationUser { get; set; }
public Guid? GrantedProjectId { get; set; }
public virtual Project GrantedProject { get; set; }
}

public class UserServiceAccountAccessPolicy : AccessPolicy
{
public Guid? OrganizationUserId { get; set; }
public virtual OrganizationUser OrganizationUser { get; set; }
public Guid? GrantedServiceAccountId { get; set; }
public virtual ServiceAccount GrantedServiceAccount { get; set; }
}

public class GroupProjectAccessPolicy : AccessPolicy
{
public Guid? GroupId { get; set; }
public virtual Group Group { get; set; }
public Guid? GrantedProjectId { get; set; }
public virtual Project GrantedProject { get; set; }
}

public class GroupServiceAccountAccessPolicy : AccessPolicy
{
public Guid? GroupId { get; set; }
public virtual Group Group { get; set; }
public Guid? GrantedServiceAccountId { get; set; }
public virtual ServiceAccount GrantedServiceAccount { get; set; }
}

public class ServiceAccountProjectAccessPolicy : AccessPolicy
{
public Guid? ServiceAccountId { get; set; }
public virtual ServiceAccount ServiceAccount { get; set; }
public Guid? GrantedProjectId { get; set; }
public virtual Project GrantedProject { get; set; }
}
3 changes: 3 additions & 0 deletions src/Infrastructure.EntityFramework/Models/Project.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ public class Project : Core.Entities.Project
{
public virtual new ICollection<Secret> Secrets { get; set; }
public virtual Organization Organization { get; set; }
public virtual ICollection<GroupProjectAccessPolicy> GroupAccessPolicies { get; set; }
public virtual ICollection<UserProjectAccessPolicy> UserAccessPolicies { get; set; }
public virtual ICollection<ServiceAccountProjectAccessPolicy> ServiceAccountAccessPolicies { get; set; }
}

public class ProjectMapperProfile : Profile
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using AutoMapper;
using Bit.Core.Repositories;
using Bit.Infrastructure.EntityFramework.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using CoreAccessPolicy = Bit.Core.Entities.AccessPolicy;

namespace Bit.Infrastructure.EntityFramework.Repositories;

public class AccessPolicyRepository : IAccessPolicyRepository
{
public AccessPolicyRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
{
}

protected Func<DatabaseContext, DbSet<AccessPolicy>> GetDbSet { get; private set; }

public Task<CoreAccessPolicy> GetByIdAsync(Guid id) => throw new NotImplementedException();

public Task<CoreAccessPolicy> CreateAsync(CoreAccessPolicy obj) => throw new NotImplementedException();

public Task ReplaceAsync(CoreAccessPolicy obj) => throw new NotImplementedException();

public Task UpsertAsync(CoreAccessPolicy obj) => throw new NotImplementedException();

public Task DeleteAsync(CoreAccessPolicy obj) => throw new NotImplementedException();
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public DatabaseContext(DbContextOptions<DatabaseContext> options)
: base(options)
{ }

public DbSet<AccessPolicy> AccessPolicies { get; set; }
public DbSet<ApiKey> ApiKeys { get; set; }
public DbSet<Cipher> Ciphers { get; set; }
public DbSet<Collection> Collections { get; set; }
Expand Down
Loading

0 comments on commit b3cf05e

Please sign in to comment.