using System.Data; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.Infrastructure.Dapper.AdminConsole.Helpers; using Dapper; using Microsoft.Data.SqlClient; #nullable enable namespace Bit.Infrastructure.Dapper.Repositories; public class CollectionRepository : Repository, ICollectionRepository { public CollectionRepository(GlobalSettings globalSettings) : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) { } public CollectionRepository(string connectionString, string readOnlyConnectionString) : base(connectionString, readOnlyConnectionString) { } public async Task GetCountByOrganizationIdAsync(Guid organizationId) { using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.ExecuteScalarAsync( "[dbo].[Collection_ReadCountByOrganizationId]", new { OrganizationId = organizationId }, commandType: CommandType.StoredProcedure); return results; } } public async Task> GetByIdWithAccessAsync(Guid id) { using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.QueryMultipleAsync( $"[{Schema}].[Collection_ReadWithGroupsAndUsersById]", new { Id = id }, commandType: CommandType.StoredProcedure); var collection = await results.ReadFirstOrDefaultAsync(); var groups = (await results.ReadAsync()).ToList(); var users = (await results.ReadAsync()).ToList(); var access = new CollectionAccessDetails { Groups = groups, Users = users }; return new Tuple(collection, access); } } public async Task> GetManyByManyIdsAsync(IEnumerable collectionIds) { using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.QueryAsync( $"[{Schema}].[Collection_ReadByIds]", new { Ids = collectionIds.ToGuidIdArrayTVP() }, commandType: CommandType.StoredProcedure); return results.ToList(); } } public async Task> GetManyByOrganizationIdAsync(Guid organizationId) { using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.QueryAsync( $"[{Schema}].[{Table}_ReadByOrganizationId]", new { OrganizationId = organizationId }, commandType: CommandType.StoredProcedure); return results.ToList(); } } public async Task> GetManySharedCollectionsByOrganizationIdAsync(Guid organizationId) { using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.QueryAsync( $"[{Schema}].[{Table}_ReadSharedCollectionsByOrganizationId]", new { OrganizationId = organizationId }, commandType: CommandType.StoredProcedure); return results.ToList(); } } public async Task>> GetManyByOrganizationIdWithAccessAsync(Guid organizationId) { using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.QueryMultipleAsync( $"[{Schema}].[Collection_ReadWithGroupsAndUsersByOrganizationId]", new { OrganizationId = organizationId }, commandType: CommandType.StoredProcedure); var collections = (await results.ReadAsync()); var groups = (await results.ReadAsync()) .GroupBy(g => g.CollectionId); var users = (await results.ReadAsync()) .GroupBy(u => u.CollectionId); return collections.Select(collection => new Tuple( collection, new CollectionAccessDetails { Groups = groups .FirstOrDefault(g => g.Key == collection.Id)? .Select(g => new CollectionAccessSelection { Id = g.GroupId, HidePasswords = g.HidePasswords, ReadOnly = g.ReadOnly, Manage = g.Manage }).ToList() ?? new List(), Users = users .FirstOrDefault(u => u.Key == collection.Id)? .Select(c => new CollectionAccessSelection { Id = c.OrganizationUserId, HidePasswords = c.HidePasswords, ReadOnly = c.ReadOnly, Manage = c.Manage }).ToList() ?? new List() } ) ).ToList(); } } public async Task> GetManyByUserIdAsync(Guid userId) { using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.QueryAsync( $"[{Schema}].[Collection_ReadByUserId]", new { UserId = userId }, commandType: CommandType.StoredProcedure); return results.ToList(); } } public async Task> GetManyByOrganizationIdWithPermissionsAsync(Guid organizationId, Guid userId, bool includeAccessRelationships) { using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.QueryMultipleAsync( $"[{Schema}].[Collection_ReadByOrganizationIdWithPermissions]", new { OrganizationId = organizationId, UserId = userId, IncludeAccessRelationships = includeAccessRelationships }, commandType: CommandType.StoredProcedure); var collections = (await results.ReadAsync()).ToList(); if (!includeAccessRelationships) { return collections; } var groups = (await results.ReadAsync()) .GroupBy(g => g.CollectionId) .ToList(); var users = (await results.ReadAsync()) .GroupBy(u => u.CollectionId) .ToList(); foreach (var collection in collections) { collection.Groups = groups .FirstOrDefault(g => g.Key == collection.Id)? .Select(g => new CollectionAccessSelection { Id = g.GroupId, HidePasswords = g.HidePasswords, ReadOnly = g.ReadOnly, Manage = g.Manage }).ToList() ?? new List(); collection.Users = users .FirstOrDefault(u => u.Key == collection.Id)? .Select(c => new CollectionAccessSelection { Id = c.OrganizationUserId, HidePasswords = c.HidePasswords, ReadOnly = c.ReadOnly, Manage = c.Manage }).ToList() ?? new List(); } return collections; } } public async Task GetByIdWithPermissionsAsync(Guid collectionId, Guid? userId, bool includeAccessRelationships) { using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.QueryMultipleAsync( $"[{Schema}].[Collection_ReadByIdWithPermissions]", new { CollectionId = collectionId, UserId = userId, IncludeAccessRelationships = includeAccessRelationships }, commandType: CommandType.StoredProcedure); var collectionDetails = await results.ReadFirstOrDefaultAsync(); if (!includeAccessRelationships || collectionDetails == null) return collectionDetails; // TODO-NRE: collectionDetails should be checked for null and probably return early collectionDetails!.Groups = (await results.ReadAsync()).ToList(); collectionDetails.Users = (await results.ReadAsync()).ToList(); return collectionDetails; } } public async Task CreateAsync(Collection obj, IEnumerable? groups, IEnumerable? users) { obj.SetNewId(); var objWithGroupsAndUsers = JsonSerializer.Deserialize(JsonSerializer.Serialize(obj))!; objWithGroupsAndUsers.Groups = groups != null ? groups.ToArrayTVP() : Enumerable.Empty().ToArrayTVP(); objWithGroupsAndUsers.Users = users != null ? users.ToArrayTVP() : Enumerable.Empty().ToArrayTVP(); using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.ExecuteAsync( $"[{Schema}].[Collection_CreateWithGroupsAndUsers]", objWithGroupsAndUsers, commandType: CommandType.StoredProcedure); } } public async Task ReplaceAsync(Collection obj, IEnumerable? groups, IEnumerable? users) { var objWithGroupsAndUsers = JsonSerializer.Deserialize(JsonSerializer.Serialize(obj))!; objWithGroupsAndUsers.Groups = groups != null ? groups.ToArrayTVP() : Enumerable.Empty().ToArrayTVP(); objWithGroupsAndUsers.Users = users != null ? users.ToArrayTVP() : Enumerable.Empty().ToArrayTVP(); using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.ExecuteAsync( $"[{Schema}].[Collection_UpdateWithGroupsAndUsers]", objWithGroupsAndUsers, commandType: CommandType.StoredProcedure); } } public async Task DeleteManyAsync(IEnumerable collectionIds) { using (var connection = new SqlConnection(ConnectionString)) { await connection.ExecuteAsync("[dbo].[Collection_DeleteByIds]", new { Ids = collectionIds.ToGuidIdArrayTVP() }, commandType: CommandType.StoredProcedure); } } public async Task CreateOrUpdateAccessForManyAsync(Guid organizationId, IEnumerable collectionIds, IEnumerable users, IEnumerable groups) { using (var connection = new SqlConnection(ConnectionString)) { var usersArray = users != null ? users.ToArrayTVP() : Enumerable.Empty().ToArrayTVP(); var groupsArray = groups != null ? groups.ToArrayTVP() : Enumerable.Empty().ToArrayTVP(); var results = await connection.ExecuteAsync( $"[{Schema}].[Collection_CreateOrUpdateAccessForMany]", new { OrganizationId = organizationId, CollectionIds = collectionIds.ToGuidIdArrayTVP(), Users = usersArray, Groups = groupsArray }, commandType: CommandType.StoredProcedure); } } public async Task CreateUserAsync(Guid collectionId, Guid organizationUserId) { using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.ExecuteAsync( $"[{Schema}].[CollectionUser_Create]", new { CollectionId = collectionId, OrganizationUserId = organizationUserId }, commandType: CommandType.StoredProcedure); } } public async Task DeleteUserAsync(Guid collectionId, Guid organizationUserId) { using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.ExecuteAsync( $"[{Schema}].[CollectionUser_Delete]", new { CollectionId = collectionId, OrganizationUserId = organizationUserId }, commandType: CommandType.StoredProcedure); } } public async Task UpdateUsersAsync(Guid id, IEnumerable users) { using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.ExecuteAsync( $"[{Schema}].[CollectionUser_UpdateUsers]", new { CollectionId = id, Users = users.ToArrayTVP() }, commandType: CommandType.StoredProcedure); } } public async Task> GetManyUsersByIdAsync(Guid id) { using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.QueryAsync( $"[{Schema}].[CollectionUser_ReadByCollectionId]", new { CollectionId = id }, commandType: CommandType.StoredProcedure); return results.ToList(); } } public async Task UpsertDefaultCollectionsAsync(Guid organizationId, IEnumerable organizationUserIds, string defaultCollectionName) { organizationUserIds = organizationUserIds.ToList(); if (!organizationUserIds.Any()) { return; } await using var connection = new SqlConnection(ConnectionString); connection.Open(); await using var transaction = connection.BeginTransaction(); try { var orgUserIdWithDefaultCollection = await GetOrgUserIdsWithDefaultCollectionAsync(connection, transaction, organizationId); var missingDefaultCollectionUserIds = organizationUserIds.Except(orgUserIdWithDefaultCollection); var (collectionUsers, collections) = BuildDefaultCollectionForUsers(organizationId, missingDefaultCollectionUserIds, defaultCollectionName); if (!collectionUsers.Any() || !collections.Any()) { return; } await BulkResourceCreationService.CreateCollectionsAsync(connection, transaction, collections); await BulkResourceCreationService.CreateCollectionsUsersAsync(connection, transaction, collectionUsers); transaction.Commit(); } catch { transaction.Rollback(); throw; } } private async Task> GetOrgUserIdsWithDefaultCollectionAsync(SqlConnection connection, SqlTransaction transaction, Guid organizationId) { const string sql = @" SELECT ou.Id AS OrganizationUserId FROM OrganizationUser ou INNER JOIN CollectionUser cu ON cu.OrganizationUserId = ou.Id INNER JOIN Collection c ON c.Id = cu.CollectionId WHERE ou.OrganizationId = @OrganizationId AND c.Type = @CollectionType; "; var organizationUserIds = await connection.QueryAsync( sql, new { OrganizationId = organizationId, CollectionType = CollectionType.DefaultUserCollection }, transaction: transaction ); return organizationUserIds.ToHashSet(); } private (List collectionUser, List collection) BuildDefaultCollectionForUsers(Guid organizationId, IEnumerable missingDefaultCollectionUserIds, string defaultCollectionName) { var collectionUsers = new List(); var collections = new List(); foreach (var orgUserId in missingDefaultCollectionUserIds) { var collectionId = CoreHelpers.GenerateComb(); collections.Add(new Collection { Id = collectionId, OrganizationId = organizationId, Name = defaultCollectionName, CreationDate = DateTime.UtcNow, RevisionDate = DateTime.UtcNow, Type = CollectionType.DefaultUserCollection, DefaultUserCollectionEmail = null }); collectionUsers.Add(new CollectionUser { CollectionId = collectionId, OrganizationUserId = orgUserId, ReadOnly = false, HidePasswords = false, Manage = true, }); } return (collectionUsers, collections); } public class CollectionWithGroupsAndUsers : Collection { [DisallowNull] public DataTable? Groups { get; set; } [DisallowNull] public DataTable? Users { get; set; } } }