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

[SM-193] Access Policy #2359

Merged
merged 17 commits into from
Dec 9, 2022
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))
Thomas-Avery marked this conversation as resolved.
Show resolved Hide resolved
.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; }
}
Hinton marked this conversation as resolved.
Show resolved Hide resolved

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