diff --git a/Directory.Build.props b/Directory.Build.props
index a98aa02c2a..bd7e8db982 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,9 +1,9 @@
+ net5.0
1.41.5
- netcoreapp3.1
Bit.$(MSBuildProjectName)
-
\ No newline at end of file
+
diff --git a/README.md b/README.md
index 9083fb1318..ea5cc68c53 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ Please read the [Setup guide](https://github.com/bitwarden/server/blob/master/SE
### Requirements
-- [.NET Core 3.1 SDK](https://www.microsoft.com/net/download/core)
+- [.NET Core 5.0 SDK](https://www.microsoft.com/net/download/core)
- [SQL Server 2017](https://docs.microsoft.com/en-us/sql/index)
*These dependencies are free to use.*
diff --git a/SETUP.md b/SETUP.md
index 77c9b43b6e..06c1ef74eb 100644
--- a/SETUP.md
+++ b/SETUP.md
@@ -91,7 +91,7 @@ User secrets are a method for managing application settings on a per-developer b
User secrets override the settings in `appsettings.json` of each project. Your user secrets file should match the structure of the `appsettings.json` file for the settings you intend to override.
-For more information, see: [Safe storage of app secrets in development in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-3.1).
+For more information, see: [Safe storage of app secrets in development in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-5.0).
Open the server solution file (`bitwarden-server.sln`) in Visual Studio before proceeding.
diff --git a/bitwarden-server.sln b/bitwarden-server.sln
index 75d09e19d2..39f7dc8211 100644
--- a/bitwarden-server.sln
+++ b/bitwarden-server.sln
@@ -72,6 +72,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommCore.Test", "bitwarden_
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test - Bitwarden License", "test - Bitwarden License", "{287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MySqlMigrations", "util\MySqlMigrations\MySqlMigrations.csproj", "{BDC1D592-5947-47ED-9903-7CDBB12A50C8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PostgresMigrations", "util\PostgresMigrations\PostgresMigrations.csproj", "{F72E0229-2EF7-49B3-9004-FF4C0043816E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -152,6 +156,15 @@ Global
{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BDC1D592-5947-47ED-9903-7CDBB12A50C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BDC1D592-5947-47ED-9903-7CDBB12A50C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BDC1D592-5947-47ED-9903-7CDBB12A50C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BDC1D592-5947-47ED-9903-7CDBB12A50C8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F72E0229-2EF7-49B3-9004-FF4C0043816E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F72E0229-2EF7-49B3-9004-FF4C0043816E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F72E0229-2EF7-49B3-9004-FF4C0043816E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F72E0229-2EF7-49B3-9004-FF4C0043816E}.Release|Any CPU.Build.0 = Release|Any CPU
+
{EDC0D688-D58C-4CE1-AA07-3606AC6874B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EDC0D688-D58C-4CE1-AA07-3606AC6874B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EDC0D688-D58C-4CE1-AA07-3606AC6874B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -183,6 +196,8 @@ Global
{BA852F18-852F-4154-973B-77D577B8CA04} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}
{4866AF64-6640-4C65-A662-A31E02FF9064} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}
{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
+ {BDC1D592-5947-47ED-9903-7CDBB12A50C8} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
+ {F72E0229-2EF7-49B3-9004-FF4C0043816E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
{EDC0D688-D58C-4CE1-AA07-3606AC6874B8} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}
{0E99A21B-684B-4C59-9831-90F775CAB6F7} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
EndGlobalSection
diff --git a/bitwarden_license/src/CommCore/CommCore.csproj b/bitwarden_license/src/CommCore/CommCore.csproj
index d77902c605..a562ebbe94 100644
--- a/bitwarden_license/src/CommCore/CommCore.csproj
+++ b/bitwarden_license/src/CommCore/CommCore.csproj
@@ -1,7 +1,7 @@
- netcoreapp3.1
+ net5.0
diff --git a/bitwarden_license/src/Portal/Dockerfile b/bitwarden_license/src/Portal/Dockerfile
index e361700faa..8668004ffb 100644
--- a/bitwarden_license/src/Portal/Dockerfile
+++ b/bitwarden_license/src/Portal/Dockerfile
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
+FROM mcr.microsoft.com/dotnet/aspnet:5.0
LABEL com.bitwarden.product="bitwarden"
diff --git a/bitwarden_license/src/Sso/Dockerfile b/bitwarden_license/src/Sso/Dockerfile
index e361700faa..8668004ffb 100644
--- a/bitwarden_license/src/Sso/Dockerfile
+++ b/bitwarden_license/src/Sso/Dockerfile
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
+FROM mcr.microsoft.com/dotnet/aspnet:5.0
LABEL com.bitwarden.product="bitwarden"
diff --git a/bitwarden_license/test/CmmCore.Test/CommCore.Test.csproj b/bitwarden_license/test/CmmCore.Test/CommCore.Test.csproj
index 50d12b4f8c..5a319e5e89 100644
--- a/bitwarden_license/test/CmmCore.Test/CommCore.Test.csproj
+++ b/bitwarden_license/test/CmmCore.Test/CommCore.Test.csproj
@@ -1,7 +1,7 @@
- netcoreapp3.1
+ net5.0
false
diff --git a/src/Admin/Dockerfile b/src/Admin/Dockerfile
index 156091819a..f5a46d67f4 100644
--- a/src/Admin/Dockerfile
+++ b/src/Admin/Dockerfile
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
+FROM mcr.microsoft.com/dotnet/aspnet:5.0
LABEL com.bitwarden.product="bitwarden"
diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj
index 84264f890f..3a5b1847f5 100644
--- a/src/Api/Api.csproj
+++ b/src/Api/Api.csproj
@@ -28,7 +28,7 @@
-
+
diff --git a/src/Api/Dockerfile b/src/Api/Dockerfile
index e361700faa..8668004ffb 100644
--- a/src/Api/Dockerfile
+++ b/src/Api/Dockerfile
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
+FROM mcr.microsoft.com/dotnet/aspnet:5.0
LABEL com.bitwarden.product="bitwarden"
diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj
index 5db7064b39..5367890044 100644
--- a/src/Core/Core.csproj
+++ b/src/Core/Core.csproj
@@ -27,18 +27,20 @@
+
-
-
+
+
-
-
-
-
-
+
+
+
+
+
+
diff --git a/src/Core/Enums/CipherStateAction.cs b/src/Core/Enums/CipherStateAction.cs
new file mode 100644
index 0000000000..1d2c1dcf6d
--- /dev/null
+++ b/src/Core/Enums/CipherStateAction.cs
@@ -0,0 +1,9 @@
+namespace Bit.Core.Enums
+{
+ public enum CipherStateAction
+ {
+ Restore,
+ SoftDelete,
+ HardDelete,
+ }
+}
diff --git a/src/Core/Enums/SupportedDatabaseProviders.cs b/src/Core/Enums/SupportedDatabaseProviders.cs
new file mode 100644
index 0000000000..301297d4b8
--- /dev/null
+++ b/src/Core/Enums/SupportedDatabaseProviders.cs
@@ -0,0 +1,10 @@
+namespace Bit.Core.Enums
+{
+ public enum SupportedDatabaseProviders
+ {
+ SqlServer,
+ MySql,
+ Postgres,
+ }
+}
+
diff --git a/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs b/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs
index e7b563b046..b3728e812a 100644
--- a/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs
+++ b/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs
@@ -52,8 +52,8 @@ namespace Bit.Core.Models.Api
public bool UseBusinessPortal => UsePolicies || UseSso; // TODO add events if needed
public bool UsersGetPremium { get; set; }
public bool SelfHost { get; set; }
- public int Seats { get; set; }
- public int MaxCollections { get; set; }
+ public int? Seats { get; set; }
+ public short? MaxCollections { get; set; }
public short? MaxStorageGb { get; set; }
public string Key { get; set; }
public OrganizationUserStatusType Status { get; set; }
diff --git a/src/Core/Models/Data/GroupWithCollections.cs b/src/Core/Models/Data/GroupWithCollections.cs
new file mode 100644
index 0000000000..a3cbe786fa
--- /dev/null
+++ b/src/Core/Models/Data/GroupWithCollections.cs
@@ -0,0 +1,10 @@
+using System.Data;
+using Bit.Core.Models.Table;
+
+namespace Bit.Core.Models.Data
+{
+ public class GroupWithCollections : Group
+ {
+ public DataTable Collections { get; set; }
+ }
+}
diff --git a/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs b/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs
index 4e6748ce11..dc5c13f2e3 100644
--- a/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs
+++ b/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs
@@ -19,8 +19,8 @@ namespace Bit.Core.Models.Data
public bool UseBusinessPortal => UsePolicies || UseSso;
public bool SelfHost { get; set; }
public bool UsersGetPremium { get; set; }
- public int Seats { get; set; }
- public int MaxCollections { get; set; }
+ public int? Seats { get; set; }
+ public short? MaxCollections { get; set; }
public short? MaxStorageGb { get; set; }
public string Key { get; set; }
public Enums.OrganizationUserStatusType Status { get; set; }
diff --git a/src/Core/Models/Data/OrganizationUserWithCollections.cs b/src/Core/Models/Data/OrganizationUserWithCollections.cs
new file mode 100644
index 0000000000..f1b7d11f64
--- /dev/null
+++ b/src/Core/Models/Data/OrganizationUserWithCollections.cs
@@ -0,0 +1,10 @@
+using System.Data;
+using Bit.Core.Models.Table;
+
+namespace Bit.Core.Models.Data
+{
+ public class OrganizationUserWithCollections : OrganizationUser
+ {
+ public DataTable Collections { get; set; }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/Cipher.cs b/src/Core/Models/EntityFramework/Cipher.cs
index 6edfe8773b..59b21cc14c 100644
--- a/src/Core/Models/EntityFramework/Cipher.cs
+++ b/src/Core/Models/EntityFramework/Cipher.cs
@@ -1,57 +1,14 @@
-using System.Text.Json;
+using System.Collections.Generic;
+using System.Text.Json;
using AutoMapper;
namespace Bit.Core.Models.EntityFramework
{
public class Cipher : Table.Cipher
{
- private JsonDocument _dataJson;
- private JsonDocument _attachmentsJson;
- private JsonDocument _favoritesJson;
- private JsonDocument _foldersJson;
-
- public User User { get; set; }
- public Organization Organization { get; set; }
- [IgnoreMap]
- public JsonDocument DataJson
- {
- get => _dataJson;
- set
- {
- Data = value?.ToString();
- _dataJson = value;
- }
- }
- [IgnoreMap]
- public JsonDocument AttachmentsJson
- {
- get => _attachmentsJson;
- set
- {
- Attachments = value?.ToString();
- _attachmentsJson = value;
- }
- }
- [IgnoreMap]
- public JsonDocument FavoritesJson
- {
- get => _favoritesJson;
- set
- {
- Favorites = value?.ToString();
- _favoritesJson = value;
- }
- }
- [IgnoreMap]
- public JsonDocument FoldersJson
- {
- get => _foldersJson;
- set
- {
- Folders = value?.ToString();
- _foldersJson = value;
- }
- }
+ public virtual User User { get; set; }
+ public virtual Organization Organization { get; set; }
+ public virtual ICollection CollectionCiphers { get; set; }
}
public class CipherMapperProfile : Profile
diff --git a/src/Core/Models/EntityFramework/Collection.cs b/src/Core/Models/EntityFramework/Collection.cs
new file mode 100644
index 0000000000..697f724d96
--- /dev/null
+++ b/src/Core/Models/EntityFramework/Collection.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework
+{
+ public class Collection : Table.Collection
+ {
+ public virtual Organization Organization { get; set; }
+ public virtual ICollection CollectionUsers { get; set; }
+ public virtual ICollection CollectionCiphers { get; set; }
+ public virtual ICollection CollectionGroups { get; set; }
+ }
+
+ public class CollectionMapperProfile : Profile
+ {
+ public CollectionMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/CollectionCipher.cs b/src/Core/Models/EntityFramework/CollectionCipher.cs
new file mode 100644
index 0000000000..00650b92d4
--- /dev/null
+++ b/src/Core/Models/EntityFramework/CollectionCipher.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework
+{
+ public class CollectionCipher : Table.CollectionCipher
+ {
+ public virtual Cipher Cipher { get; set; }
+ public virtual Collection Collection { get; set; }
+ }
+
+ public class CollectionCipherMapperProfile : Profile
+ {
+ public CollectionCipherMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/CollectionGroup.cs b/src/Core/Models/EntityFramework/CollectionGroup.cs
new file mode 100644
index 0000000000..afd714fdff
--- /dev/null
+++ b/src/Core/Models/EntityFramework/CollectionGroup.cs
@@ -0,0 +1,18 @@
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework
+{
+ public class CollectionGroup : Table.CollectionGroup
+ {
+ public virtual Collection Collection { get; set; }
+ public virtual Group Group { get; set; }
+ }
+
+ public class CollectionGroupMapperProfile : Profile
+ {
+ public CollectionGroupMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/CollectionUser.cs b/src/Core/Models/EntityFramework/CollectionUser.cs
new file mode 100644
index 0000000000..ec5a7e62ec
--- /dev/null
+++ b/src/Core/Models/EntityFramework/CollectionUser.cs
@@ -0,0 +1,18 @@
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework
+{
+ public class CollectionUser : Table.CollectionUser
+ {
+ public virtual Collection Collection { get; set; }
+ public virtual OrganizationUser OrganizationUser { get; set; }
+ }
+
+ public class CollectionUserMapperProfile : Profile
+ {
+ public CollectionUserMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/Device.cs b/src/Core/Models/EntityFramework/Device.cs
new file mode 100644
index 0000000000..a70e97cc32
--- /dev/null
+++ b/src/Core/Models/EntityFramework/Device.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework
+{
+ public class Device : Table.Device
+ {
+ public virtual User User { get; set; }
+ }
+
+ public class DeviceMapperProfile : Profile
+ {
+ public DeviceMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/EmergencyAccess.cs b/src/Core/Models/EntityFramework/EmergencyAccess.cs
new file mode 100644
index 0000000000..950e386b75
--- /dev/null
+++ b/src/Core/Models/EntityFramework/EmergencyAccess.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework
+{
+ public class EmergencyAccess : Table.EmergencyAccess
+ {
+ public virtual User Grantee { get; set; }
+ public virtual User Grantor { get; set; }
+ }
+
+ public class EmergencyAccessMapperProfile : Profile
+ {
+ public EmergencyAccessMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/Event.cs b/src/Core/Models/EntityFramework/Event.cs
new file mode 100644
index 0000000000..3e9faa2bc7
--- /dev/null
+++ b/src/Core/Models/EntityFramework/Event.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework
+{
+ public class Event : Table.Event
+ {
+ }
+
+ public class EventMapperProfile : Profile
+ {
+ public EventMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/Folder.cs b/src/Core/Models/EntityFramework/Folder.cs
new file mode 100644
index 0000000000..6e5bdaf0f9
--- /dev/null
+++ b/src/Core/Models/EntityFramework/Folder.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework
+{
+ public class Folder : Table.Folder
+ {
+ public virtual User User { get; set; }
+ }
+
+ public class FolderMapperProfile : Profile
+ {
+ public FolderMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/Grant.cs b/src/Core/Models/EntityFramework/Grant.cs
new file mode 100644
index 0000000000..e110233d9e
--- /dev/null
+++ b/src/Core/Models/EntityFramework/Grant.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework
+{
+ public class Grant : Table.Grant
+ {
+ }
+
+ public class GrantMapperProfile : Profile
+ {
+ public GrantMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/Group.cs b/src/Core/Models/EntityFramework/Group.cs
new file mode 100644
index 0000000000..122ded667b
--- /dev/null
+++ b/src/Core/Models/EntityFramework/Group.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework
+{
+ public class Group : Table.Group
+ {
+ public virtual Organization Organization { get; set; }
+ public virtual ICollection GroupUsers { get; set; }
+ }
+
+ public class GroupMapperProfile : Profile
+ {
+ public GroupMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/GroupUser.cs b/src/Core/Models/EntityFramework/GroupUser.cs
new file mode 100644
index 0000000000..55f18d8ad2
--- /dev/null
+++ b/src/Core/Models/EntityFramework/GroupUser.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework
+{
+ public class GroupUser : Table.GroupUser
+ {
+ public virtual Group Group { get; set; }
+ public virtual OrganizationUser OrganizationUser { get; set; }
+ }
+
+ public class GroupUserMapperProfile : Profile
+ {
+ public GroupUserMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
+
diff --git a/src/Core/Models/EntityFramework/Installation.cs b/src/Core/Models/EntityFramework/Installation.cs
new file mode 100644
index 0000000000..6b65fdd291
--- /dev/null
+++ b/src/Core/Models/EntityFramework/Installation.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework
+{
+ public class Installation : Table.Installation
+ {
+ }
+
+ public class InstallationMapperProfile : Profile
+ {
+ public InstallationMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/Organization.cs b/src/Core/Models/EntityFramework/Organization.cs
index 2ee5c4c190..ff3d29b9a4 100644
--- a/src/Core/Models/EntityFramework/Organization.cs
+++ b/src/Core/Models/EntityFramework/Organization.cs
@@ -1,25 +1,17 @@
using System.Collections.Generic;
-using System.Text.Json;
using AutoMapper;
namespace Bit.Core.Models.EntityFramework
{
public class Organization : Table.Organization
{
- private JsonDocument _twoFactorProvidersJson;
-
- public ICollection Ciphers { get; set; }
-
- [IgnoreMap]
- public JsonDocument TwoFactorProvidersJson
- {
- get => _twoFactorProvidersJson;
- set
- {
- TwoFactorProviders = value?.ToString();
- _twoFactorProvidersJson = value;
- }
- }
+ public virtual ICollection Ciphers { get; set; }
+ public virtual ICollection OrganizationUsers { get; set; }
+ public virtual ICollection Groups { get; set; }
+ public virtual ICollection Policies { get; set; }
+ public virtual ICollection SsoConfigs { get; set; }
+ public virtual ICollection SsoUsers { get; set; }
+ public virtual ICollection Transactions { get; set; }
}
public class OrganizationMapperProfile : Profile
diff --git a/src/Core/Models/EntityFramework/OrganizationUser.cs b/src/Core/Models/EntityFramework/OrganizationUser.cs
new file mode 100644
index 0000000000..de75f24d6e
--- /dev/null
+++ b/src/Core/Models/EntityFramework/OrganizationUser.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework
+{
+ public class OrganizationUser : Table.OrganizationUser
+ {
+ public virtual Organization Organization { get; set; }
+ public virtual User User { get; set; }
+ public virtual ICollection CollectionUsers { get; set; }
+ }
+
+ public class OrganizationUserMapperProfile : Profile
+ {
+ public OrganizationUserMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/Policy.cs b/src/Core/Models/EntityFramework/Policy.cs
new file mode 100644
index 0000000000..3646e58ad9
--- /dev/null
+++ b/src/Core/Models/EntityFramework/Policy.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework
+{
+ public class Policy : Table.Policy
+ {
+ public virtual Organization Organization { get; set; }
+ }
+
+ public class PolicyMapperProfile : Profile
+ {
+ public PolicyMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/Provider/Provider.cs b/src/Core/Models/EntityFramework/Provider/Provider.cs
new file mode 100644
index 0000000000..fc595b4498
--- /dev/null
+++ b/src/Core/Models/EntityFramework/Provider/Provider.cs
@@ -0,0 +1,16 @@
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework.Provider
+{
+ public class Provider : Table.Provider.Provider
+ {
+ }
+
+ public class ProviderMapperProfile : Profile
+ {
+ public ProviderMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/Provider/ProviderOrganization.cs b/src/Core/Models/EntityFramework/Provider/ProviderOrganization.cs
new file mode 100644
index 0000000000..5f1077d81c
--- /dev/null
+++ b/src/Core/Models/EntityFramework/Provider/ProviderOrganization.cs
@@ -0,0 +1,18 @@
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework.Provider
+{
+ public class ProviderOrganization : Table.Provider.ProviderOrganization
+ {
+ public virtual Provider Provider { get; set; }
+ public virtual Organization Organization { get; set; }
+ }
+
+ public class ProviderOrganizationMapperProfile : Profile
+ {
+ public ProviderOrganizationMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/Provider/ProviderOrganizationProviderUser.cs b/src/Core/Models/EntityFramework/Provider/ProviderOrganizationProviderUser.cs
new file mode 100644
index 0000000000..70db4fb7ed
--- /dev/null
+++ b/src/Core/Models/EntityFramework/Provider/ProviderOrganizationProviderUser.cs
@@ -0,0 +1,18 @@
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework.Provider
+{
+ public class ProviderOrganizationProviderUser : Table.Provider.ProviderOrganizationProviderUser
+ {
+ public virtual ProviderOrganization ProviderOrganization { get; set; }
+ public virtual ProviderUser ProviderUser { get; set; }
+ }
+
+ public class ProviderOrganizationProviderUserMapperProfile : Profile
+ {
+ public ProviderOrganizationProviderUserMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/Provider/ProviderUser.cs b/src/Core/Models/EntityFramework/Provider/ProviderUser.cs
new file mode 100644
index 0000000000..14d22764f7
--- /dev/null
+++ b/src/Core/Models/EntityFramework/Provider/ProviderUser.cs
@@ -0,0 +1,18 @@
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework.Provider
+{
+ public class ProviderUser : Table.Provider.ProviderUser
+ {
+ public virtual User User { get; set; }
+ public virtual Provider Provider { get; set; }
+ }
+
+ public class ProviderUserMapperProfile : Profile
+ {
+ public ProviderUserMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/Role.cs b/src/Core/Models/EntityFramework/Role.cs
new file mode 100644
index 0000000000..706e6cbd19
--- /dev/null
+++ b/src/Core/Models/EntityFramework/Role.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework
+{
+ public class Role : Table.Role
+ {
+ }
+
+ public class RoleMapperProfile : Profile
+ {
+ public RoleMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/Send.cs b/src/Core/Models/EntityFramework/Send.cs
new file mode 100644
index 0000000000..517e077496
--- /dev/null
+++ b/src/Core/Models/EntityFramework/Send.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework
+{
+ public class Send : Table.Send
+ {
+ public virtual Organization Organization { get; set; }
+ public virtual User User { get; set; }
+ }
+
+ public class SendMapperProfile : Profile
+ {
+ public SendMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/SsoConfig.cs b/src/Core/Models/EntityFramework/SsoConfig.cs
new file mode 100644
index 0000000000..74b83f6618
--- /dev/null
+++ b/src/Core/Models/EntityFramework/SsoConfig.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework
+{
+ public class SsoConfig : Table.SsoConfig
+ {
+ public virtual Organization Organization { get; set; }
+ }
+
+ public class SsoConfigMapperProfile : Profile
+ {
+ public SsoConfigMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/SsoUser.cs b/src/Core/Models/EntityFramework/SsoUser.cs
new file mode 100644
index 0000000000..616e4d8649
--- /dev/null
+++ b/src/Core/Models/EntityFramework/SsoUser.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework
+{
+ public class SsoUser : Table.SsoUser
+ {
+ public virtual Organization Organization { get; set; }
+ public virtual User User { get; set; }
+ }
+
+ public class SsoUserMapperProfile : Profile
+ {
+ public SsoUserMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/TaxRate.cs b/src/Core/Models/EntityFramework/TaxRate.cs
new file mode 100644
index 0000000000..1fe2b499e9
--- /dev/null
+++ b/src/Core/Models/EntityFramework/TaxRate.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework
+{
+ public class TaxRate : Table.TaxRate
+ {
+ }
+
+ public class TaxRateMapperProfile : Profile
+ {
+ public TaxRateMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/Transaction.cs b/src/Core/Models/EntityFramework/Transaction.cs
new file mode 100644
index 0000000000..bef6271dff
--- /dev/null
+++ b/src/Core/Models/EntityFramework/Transaction.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework
+{
+ public class Transaction : Table.Transaction
+ {
+ public virtual Organization Organization { get; set; }
+ public virtual User User { get; set; }
+ }
+
+ public class TransactionMapperProfile : Profile
+ {
+ public TransactionMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/U2f.cs b/src/Core/Models/EntityFramework/U2f.cs
new file mode 100644
index 0000000000..206baff10a
--- /dev/null
+++ b/src/Core/Models/EntityFramework/U2f.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using AutoMapper;
+
+namespace Bit.Core.Models.EntityFramework
+{
+ public class U2f : Table.U2f
+ {
+ public virtual User User { get; set; }
+ }
+
+ public class U2fMapperProfile : Profile
+ {
+ public U2fMapperProfile()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/src/Core/Models/EntityFramework/User.cs b/src/Core/Models/EntityFramework/User.cs
index 5f1aff6daa..1836bec44f 100644
--- a/src/Core/Models/EntityFramework/User.cs
+++ b/src/Core/Models/EntityFramework/User.cs
@@ -6,20 +6,14 @@ namespace Bit.Core.Models.EntityFramework
{
public class User : Table.User
{
- private JsonDocument _twoFactorProvidersJson;
-
- public ICollection Ciphers { get; set; }
-
- [IgnoreMap]
- public JsonDocument TwoFactorProvidersJson
- {
- get => _twoFactorProvidersJson;
- set
- {
- TwoFactorProviders = value?.ToString();
- _twoFactorProvidersJson = value;
- }
- }
+ public virtual ICollection Ciphers { get; set; }
+ public virtual ICollection Folders { get; set; }
+ public virtual ICollection CollectionUsers { get; set; }
+ public virtual ICollection GroupUsers { get; set; }
+ public virtual ICollection OrganizationUsers { get; set; }
+ public virtual ICollection SsoUsers { get; set; }
+ public virtual ICollection Transactions { get; set; }
+ public virtual ICollection U2fs { get; set; }
}
public class UserMapperProfile : Profile
diff --git a/src/Core/Models/Table/Collection.cs b/src/Core/Models/Table/Collection.cs
index 369592789e..85cb287386 100644
--- a/src/Core/Models/Table/Collection.cs
+++ b/src/Core/Models/Table/Collection.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel.DataAnnotations;
using Bit.Core.Utilities;
namespace Bit.Core.Models.Table
@@ -8,6 +9,7 @@ namespace Bit.Core.Models.Table
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }
public string Name { get; set; }
+ [MaxLength(300)]
public string ExternalId { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
diff --git a/src/Core/Models/Table/CollectionGroup.cs b/src/Core/Models/Table/CollectionGroup.cs
new file mode 100644
index 0000000000..b510e05e08
--- /dev/null
+++ b/src/Core/Models/Table/CollectionGroup.cs
@@ -0,0 +1,13 @@
+using System;
+using Bit.Core.Utilities;
+
+namespace Bit.Core.Models.Table
+{
+ public class CollectionGroup
+ {
+ public Guid CollectionId { get; set; }
+ public Guid GroupId { get; set; }
+ public bool ReadOnly { get; set; }
+ public bool HidePasswords { get; set; }
+ }
+}
diff --git a/src/Core/Models/Table/CollectionUser.cs b/src/Core/Models/Table/CollectionUser.cs
new file mode 100644
index 0000000000..3bcf15d706
--- /dev/null
+++ b/src/Core/Models/Table/CollectionUser.cs
@@ -0,0 +1,13 @@
+using System;
+using Bit.Core.Utilities;
+
+namespace Bit.Core.Models.Table
+{
+ public class CollectionUser
+ {
+ public Guid CollectionId { get; set; }
+ public Guid OrganizationUserId { get; set; }
+ public bool ReadOnly { get; set; }
+ public bool HidePasswords { get; set; }
+ }
+}
diff --git a/src/Core/Models/Table/Device.cs b/src/Core/Models/Table/Device.cs
index 4fb20652df..b4e740e5be 100644
--- a/src/Core/Models/Table/Device.cs
+++ b/src/Core/Models/Table/Device.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel.DataAnnotations;
using Bit.Core.Utilities;
namespace Bit.Core.Models.Table
@@ -7,9 +8,12 @@ namespace Bit.Core.Models.Table
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
+ [MaxLength(50)]
public string Name { get; set; }
public Enums.DeviceType Type { get; set; }
+ [MaxLength(50)]
public string Identifier { get; set; }
+ [MaxLength(255)]
public string PushToken { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
diff --git a/src/Core/Models/Table/EmergencyAccess.cs b/src/Core/Models/Table/EmergencyAccess.cs
index fe118ca76a..0e256cff37 100644
--- a/src/Core/Models/Table/EmergencyAccess.cs
+++ b/src/Core/Models/Table/EmergencyAccess.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
using Bit.Core.Utilities;
@@ -9,6 +10,7 @@ namespace Bit.Core.Models.Table
public Guid Id { get; set; }
public Guid GrantorId { get; set; }
public Guid? GranteeId { get; set; }
+ [MaxLength(256)]
public string Email { get; set; }
public string KeyEncrypted { get; set; }
public EmergencyAccessType Type { get; set; }
diff --git a/src/Core/Models/Table/Event.cs b/src/Core/Models/Table/Event.cs
index 6b00a7b81c..eb68289fb8 100644
--- a/src/Core/Models/Table/Event.cs
+++ b/src/Core/Models/Table/Event.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Utilities;
@@ -36,6 +37,7 @@ namespace Bit.Core.Models.Table
public Guid? GroupId { get; set; }
public Guid? OrganizationUserId { get; set; }
public DeviceType? DeviceType { get; set; }
+ [MaxLength(50)]
public string IpAddress { get; set; }
public Guid? ActingUserId { get; set; }
diff --git a/src/Core/Models/Table/Grant.cs b/src/Core/Models/Table/Grant.cs
index 785fa5d1a0..e8b052d114 100644
--- a/src/Core/Models/Table/Grant.cs
+++ b/src/Core/Models/Table/Grant.cs
@@ -1,14 +1,21 @@
using System;
+using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Table
{
public class Grant
{
+ [MaxLength(200)]
public string Key { get; set; }
+ [MaxLength(50)]
public string Type { get; set; }
+ [MaxLength(200)]
public string SubjectId { get; set; }
+ [MaxLength(100)]
public string SessionId { get; set; }
+ [MaxLength(200)]
public string ClientId { get; set; }
+ [MaxLength(200)]
public string Description { get; set; }
public DateTime CreationDate { get; set; }
public DateTime? ExpirationDate { get; set; }
diff --git a/src/Core/Models/Table/Group.cs b/src/Core/Models/Table/Group.cs
index bdb72b4d5d..d732746886 100644
--- a/src/Core/Models/Table/Group.cs
+++ b/src/Core/Models/Table/Group.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel.DataAnnotations;
using Bit.Core.Utilities;
namespace Bit.Core.Models.Table
@@ -7,8 +8,10 @@ namespace Bit.Core.Models.Table
{
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }
+ [MaxLength(100)]
public string Name { get; set; }
public bool AccessAll { get; set; }
+ [MaxLength(300)]
public string ExternalId { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
diff --git a/src/Core/Models/Table/Installation.cs b/src/Core/Models/Table/Installation.cs
index e7abc7d0d7..631fa5bf6d 100644
--- a/src/Core/Models/Table/Installation.cs
+++ b/src/Core/Models/Table/Installation.cs
@@ -1,12 +1,15 @@
using Bit.Core.Utilities;
using System;
+using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Table
{
public class Installation : ITableObject
{
public Guid Id { get; set; }
+ [MaxLength(256)]
public string Email { get; set; }
+ [MaxLength(150)]
public string Key { get; set; }
public bool Enabled { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
diff --git a/src/Core/Models/Table/Organization.cs b/src/Core/Models/Table/Organization.cs
index 6af3e46656..4a23bcd163 100644
--- a/src/Core/Models/Table/Organization.cs
+++ b/src/Core/Models/Table/Organization.cs
@@ -4,6 +4,7 @@ using Bit.Core.Enums;
using System.Collections.Generic;
using Newtonsoft.Json;
using System.Linq;
+using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Table
{
@@ -12,15 +13,25 @@ namespace Bit.Core.Models.Table
private Dictionary _twoFactorProviders;
public Guid Id { get; set; }
+ [MaxLength(50)]
public string Identifier { get; set; }
+ [MaxLength(50)]
public string Name { get; set; }
+ [MaxLength(50)]
public string BusinessName { get; set; }
+ [MaxLength(50)]
public string BusinessAddress1 { get; set; }
+ [MaxLength(50)]
public string BusinessAddress2 { get; set; }
+ [MaxLength(50)]
public string BusinessAddress3 { get; set; }
+ [MaxLength(2)]
public string BusinessCountry { get; set; }
+ [MaxLength(30)]
public string BusinessTaxNumber { get; set; }
+ [MaxLength(256)]
public string BillingEmail { get; set; }
+ [MaxLength(50)]
public string Plan { get; set; }
public PlanType PlanType { get; set; }
public int? Seats { get; set; }
@@ -39,11 +50,15 @@ namespace Bit.Core.Models.Table
public long? Storage { get; set; }
public short? MaxStorageGb { get; set; }
public GatewayType? Gateway { get; set; }
+ [MaxLength(50)]
public string GatewayCustomerId { get; set; }
+ [MaxLength(50)]
public string GatewaySubscriptionId { get; set; }
public string ReferenceData { get; set; }
public bool Enabled { get; set; } = true;
+ [MaxLength(100)]
public string LicenseKey { get; set; }
+ [MaxLength(30)]
public string ApiKey { get; set; }
public string PublicKey { get; set; }
public string PrivateKey { get; set; }
diff --git a/src/Core/Models/Table/OrganizationUser.cs b/src/Core/Models/Table/OrganizationUser.cs
index 13ab71ab3f..5a275ec753 100644
--- a/src/Core/Models/Table/OrganizationUser.cs
+++ b/src/Core/Models/Table/OrganizationUser.cs
@@ -1,6 +1,7 @@
using System;
using Bit.Core.Utilities;
using Bit.Core.Enums;
+using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Table
{
@@ -9,12 +10,14 @@ namespace Bit.Core.Models.Table
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }
public Guid? UserId { get; set; }
+ [MaxLength(256)]
public string Email { get; set; }
public string Key { get; set; }
public string ResetPasswordKey { get; set; }
public OrganizationUserStatusType Status { get; set; }
public OrganizationUserType Type { get; set; }
public bool AccessAll { get; set; }
+ [MaxLength(300)]
public string ExternalId { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
diff --git a/src/Core/Models/Table/Send.cs b/src/Core/Models/Table/Send.cs
index 2660e9e8a6..36607d965f 100644
--- a/src/Core/Models/Table/Send.cs
+++ b/src/Core/Models/Table/Send.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
using Bit.Core.Utilities;
@@ -12,6 +13,7 @@ namespace Bit.Core.Models.Table
public SendType Type { get; set; }
public string Data { get; set; }
public string Key { get; set; }
+ [MaxLength(300)]
public string Password { get; set; }
public int? MaxAccessCount { get; set; }
public int AccessCount { get; set; }
diff --git a/src/Core/Models/Table/SsoConfig.cs b/src/Core/Models/Table/SsoConfig.cs
index 5e5d47f4a2..6a1b232126 100644
--- a/src/Core/Models/Table/SsoConfig.cs
+++ b/src/Core/Models/Table/SsoConfig.cs
@@ -13,7 +13,8 @@ namespace Bit.Core.Models.Table
public void SetNewId()
{
- // nothing - int will be auto-populated
+ // int will be auto-populated
+ Id = 0;
}
}
}
diff --git a/src/Core/Models/Table/SsoUser.cs b/src/Core/Models/Table/SsoUser.cs
index da4e38528e..6c7cd6257f 100644
--- a/src/Core/Models/Table/SsoUser.cs
+++ b/src/Core/Models/Table/SsoUser.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Table
{
@@ -7,12 +8,14 @@ namespace Bit.Core.Models.Table
public long Id { get; set; }
public Guid UserId { get; set; }
public Guid? OrganizationId { get; set; }
+ [MaxLength(50)]
public string ExternalId { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public void SetNewId()
{
- // nothing - int will be auto-populated
+ // int will be auto-populated
+ Id = 0;
}
}
}
diff --git a/src/Core/Models/Table/TaxRate.cs b/src/Core/Models/Table/TaxRate.cs
index 195465efda..a9a7585774 100644
--- a/src/Core/Models/Table/TaxRate.cs
+++ b/src/Core/Models/Table/TaxRate.cs
@@ -1,10 +1,16 @@
+using System.ComponentModel.DataAnnotations;
+
namespace Bit.Core.Models.Table
{
public class TaxRate: ITableObject
{
+ [MaxLength(40)]
public string Id { get; set; }
+ [MaxLength(50)]
public string Country { get; set; }
+ [MaxLength(2)]
public string State { get; set; }
+ [MaxLength(10)]
public string PostalCode { get; set; }
public decimal Rate { get; set; }
public bool Active { get; set; }
diff --git a/src/Core/Models/Table/Transaction.cs b/src/Core/Models/Table/Transaction.cs
index 637f40f403..0f5e80e457 100644
--- a/src/Core/Models/Table/Transaction.cs
+++ b/src/Core/Models/Table/Transaction.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
using Bit.Core.Utilities;
@@ -13,9 +14,11 @@ namespace Bit.Core.Models.Table
public decimal Amount { get; set; }
public bool? Refunded { get; set; }
public decimal? RefundedAmount { get; set; }
+ [MaxLength(100)]
public string Details { get; set; }
public PaymentMethodType? PaymentMethodType { get; set; }
public GatewayType? Gateway { get; set; }
+ [MaxLength(50)]
public string GatewayId { get; set; }
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
diff --git a/src/Core/Models/Table/U2f.cs b/src/Core/Models/Table/U2f.cs
index e8a97c7fe5..5d089dacaa 100644
--- a/src/Core/Models/Table/U2f.cs
+++ b/src/Core/Models/Table/U2f.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Table
{
@@ -6,15 +7,20 @@ namespace Bit.Core.Models.Table
{
public int Id { get; set; }
public Guid UserId { get; set; }
+ [MaxLength(200)]
public string KeyHandle { get; set; }
+ [MaxLength(200)]
public string Challenge { get; set; }
+ [MaxLength(50)]
public string AppId { get; set; }
+ [MaxLength(20)]
public string Version { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public void SetNewId()
{
- // do nothing since it is an identity
+ // int will be auto-populated
+ Id = 0;
}
}
}
diff --git a/src/Core/Models/Table/User.cs b/src/Core/Models/Table/User.cs
index 20513e0967..12c3f50d28 100644
--- a/src/Core/Models/Table/User.cs
+++ b/src/Core/Models/Table/User.cs
@@ -4,6 +4,7 @@ using Bit.Core.Utilities;
using System.Collections.Generic;
using Newtonsoft.Json;
using Microsoft.AspNetCore.Identity;
+using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Table
{
@@ -12,14 +13,23 @@ namespace Bit.Core.Models.Table
private Dictionary _twoFactorProviders;
public Guid Id { get; set; }
+ [MaxLength(50)]
public string Name { get; set; }
+ [Required]
+ [MaxLength(256)]
public string Email { get; set; }
public bool EmailVerified { get; set; }
+ [MaxLength(300)]
public string MasterPassword { get; set; }
+ [MaxLength(50)]
public string MasterPasswordHint { get; set; }
+ [MaxLength(10)]
public string Culture { get; set; } = "en-US";
+ [Required]
+ [MaxLength(50)]
public string SecurityStamp { get; set; }
public string TwoFactorProviders { get; set; }
+ [MaxLength(32)]
public string TwoFactorRecoveryCode { get; set; }
public string EquivalentDomains { get; set; }
public string ExcludedGlobalEquivalentDomains { get; set; }
@@ -33,10 +43,15 @@ namespace Bit.Core.Models.Table
public long? Storage { get; set; }
public short? MaxStorageGb { get; set; }
public GatewayType? Gateway { get; set; }
+ [MaxLength(50)]
public string GatewayCustomerId { get; set; }
+ [MaxLength(50)]
public string GatewaySubscriptionId { get; set; }
public string ReferenceData { get; set; }
+ [MaxLength(100)]
public string LicenseKey { get; set; }
+ [Required]
+ [MaxLength(30)]
public string ApiKey { get; set; }
public KdfType Kdf { get; set; } = KdfType.PBKDF2_SHA256;
public int KdfIterations { get; set; } = 5000;
diff --git a/src/Core/Repositories/EntityFramework/BaseEntityFrameworkRepository.cs b/src/Core/Repositories/EntityFramework/BaseEntityFrameworkRepository.cs
index fd20430111..d1b20764de 100644
--- a/src/Core/Repositories/EntityFramework/BaseEntityFrameworkRepository.cs
+++ b/src/Core/Repositories/EntityFramework/BaseEntityFrameworkRepository.cs
@@ -1,10 +1,28 @@
using AutoMapper;
+using Bit.Core.Models.Table;
+using Bit.Core.Repositories.EntityFramework.Queries;
+using EfModel = Bit.Core.Models.EntityFramework;
+using LinqToDB.Data;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Threading.Tasks;
+using System;
+using Bit.Core.Enums;
+using Bit.Core.Enums.Provider;
namespace Bit.Core.Repositories.EntityFramework
{
public abstract class BaseEntityFrameworkRepository
{
+ protected BulkCopyOptions DefaultBulkCopyOptions { get; set; } = new BulkCopyOptions
+ {
+ KeepIdentity = true,
+ BulkCopyType = BulkCopyType.MultipleRows,
+ };
+
public BaseEntityFrameworkRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
{
ServiceScopeFactory = serviceScopeFactory;
@@ -18,5 +36,233 @@ namespace Bit.Core.Repositories.EntityFramework
{
return serviceScope.ServiceProvider.GetRequiredService();
}
+
+ public void ClearChangeTracking()
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ dbContext.ChangeTracker.Clear();
+ }
+ }
+
+ public async Task GetCountFromQuery(IQuery query)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ return await query.Run(GetDatabaseContext(scope)).CountAsync();
+ }
+ }
+
+ protected async Task UserBumpAccountRevisionDateByCipherId(Cipher cipher)
+ {
+ var list = new List { cipher };
+ await UserBumpAccountRevisionDateByCipherId(list);
+ }
+
+ protected async Task UserBumpAccountRevisionDateByCipherId(IEnumerable ciphers)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ foreach (var cipher in ciphers)
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = new UserBumpAccountRevisionDateByCipherIdQuery(cipher);
+ var users = query.Run(dbContext);
+
+ await users.ForEachAsync(e =>
+ {
+ dbContext.Attach(e);
+ e.RevisionDate = DateTime.UtcNow;
+ });
+ await dbContext.SaveChangesAsync();
+ }
+ }
+ }
+
+ protected async Task UserBumpAccountRevisionDateByOrganizationId(Guid organizationId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = new UserBumpAccountRevisionDateByOrganizationIdQuery(organizationId);
+ var users = query.Run(dbContext);
+ await users.ForEachAsync(e =>
+ {
+ dbContext.Attach(e);
+ e.RevisionDate = DateTime.UtcNow;
+ });
+ await dbContext.SaveChangesAsync();
+ }
+ }
+
+ protected async Task UserBumpAccountRevisionDate(Guid userId)
+ {
+ await UserBumpManyAccountRevisionDates(new[] { userId });
+ }
+
+ protected async Task UserBumpManyAccountRevisionDates(ICollection userIds)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var users = dbContext.Users.Where(u => userIds.Contains(u.Id));
+ await users.ForEachAsync(u =>
+ {
+ dbContext.Attach(u);
+ u.RevisionDate = DateTime.UtcNow;
+ });
+ await dbContext.SaveChangesAsync();
+ }
+ }
+
+ protected async Task OrganizationUpdateStorage(Guid organizationId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var attachments = await dbContext.Ciphers
+ .Where(e => e.UserId == null &&
+ e.OrganizationId == organizationId &&
+ !string.IsNullOrWhiteSpace(e.Attachments))
+ .Select(e => e.Attachments)
+ .ToListAsync();
+ var storage = attachments.Sum(e => JsonDocument.Parse(e)?.RootElement.EnumerateArray()
+ .Sum(p => p.GetProperty("Size").GetInt64()) ?? 0);
+ var organization = new EfModel.Organization
+ {
+ Id = organizationId,
+ RevisionDate = DateTime.UtcNow,
+ Storage = storage,
+ };
+ dbContext.Organizations.Attach(organization);
+ var entry = dbContext.Entry(organization);
+ entry.Property(e => e.RevisionDate).IsModified = true;
+ entry.Property(e => e.Storage).IsModified = true;
+ await dbContext.SaveChangesAsync();
+ }
+ }
+
+ protected async Task UserUpdateStorage(Guid userId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var attachments = await dbContext.Ciphers
+ .Where(e => e.UserId.HasValue &&
+ e.UserId.Value == userId &&
+ e.OrganizationId == null &&
+ !string.IsNullOrWhiteSpace(e.Attachments))
+ .Select(e => e.Attachments)
+ .ToListAsync();
+ var storage = attachments.Sum(e => JsonDocument.Parse(e)?.RootElement.EnumerateArray()
+ .Sum(p => p.GetProperty("Size").GetInt64()) ?? 0);
+ var user = new EfModel.User
+ {
+ Id = userId,
+ RevisionDate = DateTime.UtcNow,
+ Storage = storage,
+ };
+ dbContext.Users.Attach(user);
+ var entry = dbContext.Entry(user);
+ entry.Property(e => e.RevisionDate).IsModified = true;
+ entry.Property(e => e.Storage).IsModified = true;
+ await dbContext.SaveChangesAsync();
+ }
+ }
+
+ protected async Task UserUpdateKeys(User user)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var entity = await dbContext.Users.FindAsync(user.Id);
+ if (entity == null)
+ {
+ return;
+ }
+ entity.SecurityStamp = user.SecurityStamp;
+ entity.Key = user.Key;
+ entity.PrivateKey = user.PrivateKey;
+ entity.RevisionDate = DateTime.UtcNow;
+ await dbContext.SaveChangesAsync();
+ }
+ }
+
+ protected async Task UserBumpAccountRevisionDateByCollectionId(Guid collectionId, Guid organizationId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = from u in dbContext.Users
+ join ou in dbContext.OrganizationUsers
+ on u.Id equals ou.UserId
+ join cu in dbContext.CollectionUsers
+ on ou.Id equals cu.OrganizationUserId into cu_g
+ from cu in cu_g.DefaultIfEmpty()
+ where !ou.AccessAll && cu.CollectionId.Equals(collectionId)
+ join gu in dbContext.GroupUsers
+ on ou.Id equals gu.OrganizationUserId into gu_g
+ from gu in gu_g.DefaultIfEmpty()
+ where cu.CollectionId == default(Guid) && !ou.AccessAll
+ join g in dbContext.Groups
+ on gu.GroupId equals g.Id into g_g
+ from g in g_g.DefaultIfEmpty()
+ join cg in dbContext.CollectionGroups
+ on gu.GroupId equals cg.GroupId into cg_g
+ from cg in cg_g.DefaultIfEmpty()
+ where !g.AccessAll && cg.CollectionId == collectionId &&
+ (ou.OrganizationId == organizationId && ou.Status == OrganizationUserStatusType.Confirmed &&
+ (cu.CollectionId != default(Guid) || cg.CollectionId != default(Guid) || ou.AccessAll || g.AccessAll))
+ select new { u, ou, cu, gu, g, cg };
+ var users = query.Select(x => x.u);
+ await users.ForEachAsync(u =>
+ {
+ dbContext.Attach(u);
+ u.RevisionDate = DateTime.UtcNow;
+ });
+ await dbContext.SaveChangesAsync();
+ }
+ }
+
+ protected async Task UserBumpAccountRevisionDateByOrganizationUserId(Guid organizationUserId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = from u in dbContext.Users
+ join ou in dbContext.OrganizationUsers
+ on u.Id equals ou.UserId
+ where ou.Id.Equals(organizationUserId) && ou.Status.Equals(OrganizationUserStatusType.Confirmed)
+ select new { u, ou };
+ var users = query.Select(x => x.u);
+ await users.ForEachAsync(u =>
+ {
+ dbContext.Attach(u);
+ u.AccountRevisionDate = DateTime.UtcNow;
+ });
+ await dbContext.SaveChangesAsync();
+ }
+ }
+
+ protected async Task UserBumpAccountRevisionDateByProviderUserIds(ICollection providerUserIds)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = from pu in dbContext.ProviderUsers
+ join u in dbContext.Users
+ on pu.UserId equals u.Id
+ where pu.Status.Equals(ProviderUserStatusType.Confirmed) &&
+ providerUserIds.Contains(pu.Id)
+ select new { pu, u };
+ var users = query.Select(x => x.u);
+ await users.ForEachAsync(u => {
+ dbContext.Attach(u);
+ u.AccountRevisionDate = DateTime.UtcNow;
+ });
+ await dbContext.SaveChangesAsync();
+ }
+ }
}
}
diff --git a/src/Core/Repositories/EntityFramework/CipherRepository.cs b/src/Core/Repositories/EntityFramework/CipherRepository.cs
new file mode 100644
index 0000000000..f365d0da02
--- /dev/null
+++ b/src/Core/Repositories/EntityFramework/CipherRepository.cs
@@ -0,0 +1,642 @@
+using AutoMapper;
+using Bit.Core.Models.Data;
+using Bit.Core.Models.Table;
+using Bit.Core.Repositories.EntityFramework.Queries;
+using Bit.Core.Utilities;
+using Core.Models.Data;
+using EfModel = Bit.Core.Models.EntityFramework;
+using LinqToDB.Data;
+using LinqToDB.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Newtonsoft.Json.Linq;
+using Newtonsoft.Json;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using System;
+using TableModel = Bit.Core.Models.Table;
+using Bit.Core.Enums;
+
+namespace Bit.Core.Repositories.EntityFramework
+{
+ public class CipherRepository : Repository, ICipherRepository
+ {
+ public CipherRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
+ : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.Ciphers)
+ { }
+
+ public override async Task CreateAsync(Cipher cipher)
+ {
+ cipher = await base.CreateAsync(cipher);
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ if (cipher.OrganizationId.HasValue)
+ {
+ await UserBumpAccountRevisionDateByCipherId(cipher);
+ }
+ else if (cipher.UserId.HasValue)
+ {
+ await UserBumpAccountRevisionDate(cipher.UserId.Value);
+ }
+ }
+ return cipher;
+ }
+
+ public IQueryable GetBumpedAccountsByCipherId(Cipher cipher)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = new UserBumpAccountRevisionDateByCipherIdQuery(cipher);
+ return query.Run(dbContext);
+ }
+ }
+
+ public async Task CreateAsync(Cipher cipher, IEnumerable collectionIds)
+ {
+ cipher = await base.CreateAsync(cipher);
+ await UpdateCollections(cipher, collectionIds);
+ }
+
+ private async Task UpdateCollections(Cipher cipher, IEnumerable collectionIds)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var cipherEntity = await dbContext.Ciphers.FindAsync(cipher.Id);
+ var query = new CipherUpdateCollectionsQuery(cipherEntity, collectionIds).Run(dbContext);
+ await dbContext.AddRangeAsync(query);
+ await dbContext.SaveChangesAsync();
+ }
+ }
+
+ public async Task CreateAsync(CipherDetails cipher)
+ {
+ await CreateAsyncReturnCipher(cipher);
+ }
+
+ private async Task CreateAsyncReturnCipher(CipherDetails cipher)
+ {
+ cipher.SetNewId();
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var userIdKey = $"\"{cipher.UserId}\"";
+ cipher.UserId = cipher.OrganizationId.HasValue ? null : cipher.UserId;
+ cipher.Favorites = cipher.Favorite ?
+ $"{{{userIdKey}:true}}" :
+ null;
+ cipher.Folders = cipher.FolderId.HasValue ?
+ $"{{{userIdKey}:\"{cipher.FolderId}\"}}" :
+ null;
+ var entity = Mapper.Map((TableModel.Cipher)cipher);
+ await dbContext.AddAsync(entity);
+ await dbContext.SaveChangesAsync();
+ }
+ await UserBumpAccountRevisionDateByCipherId(cipher);
+ return cipher;
+ }
+
+ public async Task CreateAsync(CipherDetails cipher, IEnumerable collectionIds)
+ {
+ cipher = await CreateAsyncReturnCipher(cipher);
+ await UpdateCollections(cipher, collectionIds);
+ }
+
+ public async Task CreateAsync(IEnumerable ciphers, IEnumerable folders)
+ {
+ if (!ciphers.Any())
+ {
+ return;
+ }
+
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var folderEntities = Mapper.Map>(folders);
+ await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, folderEntities);
+ var cipherEntities = Mapper.Map>(ciphers);
+ await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, cipherEntities);
+ await UserBumpAccountRevisionDateByCipherId(ciphers);
+ }
+ }
+
+ public async Task CreateAsync(IEnumerable ciphers, IEnumerable collections, IEnumerable collectionCiphers)
+ {
+ if (!ciphers.Any())
+ {
+ return;
+ }
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var cipherEntities = Mapper.Map>(ciphers);
+ await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, cipherEntities);
+ if (collections.Any())
+ {
+ var collectionEntities = Mapper.Map>(collections);
+ await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, collectionEntities);
+
+ if (collectionCiphers.Any())
+ {
+ var collectionCipherEntities = Mapper.Map>(collectionCiphers);
+ await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, collectionCipherEntities);
+ }
+ }
+ await UserBumpAccountRevisionDateByOrganizationId(ciphers.First().OrganizationId.Value);
+ }
+ }
+
+ public async Task DeleteAsync(IEnumerable ids, Guid userId)
+ {
+ await ToggleCipherStates(ids, userId, CipherStateAction.HardDelete);
+ }
+
+ public async Task DeleteAttachmentAsync(Guid cipherId, string attachmentId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var cipher = await dbContext.Ciphers.FindAsync(cipherId);
+ var attachmentsJson = JObject.Parse(cipher.Attachments);
+ attachmentsJson.Remove(attachmentId);
+ cipher.Attachments = JsonConvert.SerializeObject(attachmentsJson);
+ await dbContext.SaveChangesAsync();
+
+ if (cipher.OrganizationId.HasValue)
+ {
+ await OrganizationUpdateStorage(cipher.OrganizationId.Value);
+ await UserBumpAccountRevisionDateByCipherId(cipher);
+ }
+ else if (cipher.UserId.HasValue)
+ {
+ await UserUpdateStorage(cipher.UserId.Value);
+ await UserBumpAccountRevisionDate(cipher.UserId.Value);
+ }
+ }
+ }
+
+ public async Task DeleteByIdsOrganizationIdAsync(IEnumerable ids, Guid organizationId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var ciphers = from c in dbContext.Ciphers
+ where c.OrganizationId == organizationId &&
+ ids.Contains(c.Id)
+ select c;
+ dbContext.RemoveRange(ciphers);
+ await dbContext.SaveChangesAsync();
+ }
+ await OrganizationUpdateStorage(organizationId);
+ await UserBumpAccountRevisionDateByOrganizationId(organizationId);
+ }
+
+ public async Task DeleteByOrganizationIdAsync(Guid organizationId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+
+ var collectionCiphers = from cc in dbContext.CollectionCiphers
+ join c in dbContext.Collections
+ on cc.CollectionId equals c.Id
+ where c.OrganizationId == organizationId
+ select cc;
+ dbContext.RemoveRange(collectionCiphers);
+
+ var ciphers = from c in dbContext.Ciphers
+ where c.OrganizationId == organizationId
+ select c;
+ dbContext.RemoveRange(ciphers);
+
+ await dbContext.SaveChangesAsync();
+ }
+ await OrganizationUpdateStorage(organizationId);
+ await UserBumpAccountRevisionDateByOrganizationId(organizationId);
+ }
+
+ public async Task DeleteByUserIdAsync(Guid userId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var ciphers = from c in dbContext.Ciphers
+ where c.UserId == userId
+ select c;
+ dbContext.RemoveRange(ciphers);
+ var folders = from f in dbContext.Folders
+ where f.UserId == userId
+ select f;
+ dbContext.RemoveRange(folders);
+ await dbContext.SaveChangesAsync();
+ await UserUpdateStorage(userId);
+ await UserBumpAccountRevisionDate(userId);
+ }
+
+ }
+
+ public async Task DeleteDeletedAsync(DateTime deletedDateBefore)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = dbContext.Ciphers.Where(c => c.DeletedDate < deletedDateBefore);
+ dbContext.RemoveRange(query);
+ await dbContext.SaveChangesAsync();
+ }
+ }
+
+ public async Task GetByIdAsync(Guid id, Guid userId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var userCipherDetails = new UserCipherDetailsQuery(userId);
+ var data = await userCipherDetails.Run(dbContext).FirstOrDefaultAsync(c => c.Id == id);
+ return data;
+ }
+ }
+
+ public async Task GetCanEditByIdAsync(Guid userId, Guid cipherId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = new CipherReadCanEditByIdUserIdQuery(userId, cipherId);
+ var canEdit = await query.Run(dbContext).AnyAsync();
+ return canEdit;
+ }
+ }
+
+ public async Task> GetManyByOrganizationIdAsync(Guid organizationId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = dbContext.Ciphers.Where(x => !x.UserId.HasValue && x.OrganizationId == organizationId);
+ var data = await query.ToListAsync();
+ return Mapper.Map>(data);
+ }
+ }
+
+ public async Task> GetManyByUserIdAsync(Guid userId, bool withOrganizations = true)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ IQueryable cipherDetailsView = withOrganizations ?
+ new UserCipherDetailsQuery(userId).Run(dbContext) :
+ new CipherDetailsQuery(userId).Run(dbContext);
+ if (!withOrganizations)
+ {
+ cipherDetailsView = from c in cipherDetailsView
+ where c.UserId == userId
+ select new CipherDetails {
+ Id = c.Id,
+ UserId = c.UserId,
+ OrganizationId = c.OrganizationId,
+ Type= c.Type,
+ Data = c.Data,
+ Attachments = c.Attachments,
+ CreationDate = c.CreationDate,
+ RevisionDate = c.RevisionDate,
+ DeletedDate = c.DeletedDate,
+ Favorite = c.Favorite,
+ FolderId = c.FolderId,
+ Edit = true,
+ ViewPassword = true,
+ OrganizationUseTotp = false,
+ };
+ }
+ var ciphers = await cipherDetailsView.ToListAsync();
+ return ciphers;
+ }
+ }
+
+ public async Task GetOrganizationDetailsByIdAsync(Guid id)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = new CipherOrganizationDetailsReadByIdQuery(id);
+ var data = await query.Run(dbContext).FirstOrDefaultAsync();
+ return data;
+ }
+ }
+
+ public async Task MoveAsync(IEnumerable ids, Guid? folderId, Guid userId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var cipherEntities = dbContext.Ciphers.Where(c => ids.Contains(c.Id));
+ var userCipherDetails = new UserCipherDetailsQuery(userId).Run(dbContext);
+ var idsToMove = from ucd in userCipherDetails
+ join c in cipherEntities
+ on ucd.Id equals c.Id
+ where ucd.Edit
+ select c;
+ await idsToMove.ForEachAsync(cipher =>
+ {
+ var foldersJson = string.IsNullOrWhiteSpace(cipher.Folders) ?
+ new JObject() :
+ JObject.Parse(cipher.Folders);
+
+ if (folderId.HasValue)
+ {
+ foldersJson.Remove(userId.ToString());
+ foldersJson.Add(userId.ToString(), folderId.Value.ToString());
+ }
+ else if (!string.IsNullOrWhiteSpace(cipher.Folders))
+ {
+ foldersJson.Remove(userId.ToString());
+ }
+ dbContext.Attach(cipher);
+ cipher.Folders = JsonConvert.SerializeObject(foldersJson);
+ });
+ await dbContext.SaveChangesAsync();
+ await UserBumpAccountRevisionDate(userId);
+ }
+ }
+
+ public async Task ReplaceAsync(CipherDetails cipher)
+ {
+ cipher.UserId = cipher.OrganizationId.HasValue ?
+ null :
+ cipher.UserId;
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var entity = await dbContext.Ciphers.FindAsync(cipher.Id);
+ if (entity != null)
+ {
+ var userIdKey = $"\"{cipher.UserId}\"";
+ if (cipher.Favorite)
+ {
+ if (cipher.Favorites == null)
+ {
+ cipher.Favorites = $"{{{userIdKey}:true}}";
+ }
+ else
+ {
+ var favorites = CoreHelpers.LoadClassFromJsonData>(cipher.Favorites);
+ favorites.Add(cipher.UserId.Value, true);
+ cipher.Favorites = JsonConvert.SerializeObject(favorites);
+ }
+ }
+ else
+ {
+ if (cipher.Favorites != null && cipher.Favorites.Contains(cipher.UserId.Value.ToString()))
+ {
+ var favorites = CoreHelpers.LoadClassFromJsonData>(cipher.Favorites);
+ favorites.Remove(cipher.UserId.Value);
+ cipher.Favorites = JsonConvert.SerializeObject(favorites);
+ }
+ }
+ if (cipher.FolderId.HasValue)
+ {
+ if (cipher.Folders == null)
+ {
+ cipher.Folders = $"{{{userIdKey}:\"{cipher.FolderId}\"}}";
+ }
+ else
+ {
+ var folders = CoreHelpers.LoadClassFromJsonData>(cipher.Folders);
+ folders.Add(cipher.UserId.Value, cipher.FolderId.Value);
+ cipher.Folders = JsonConvert.SerializeObject(folders);
+ }
+ }
+ else
+ {
+ if (cipher.Folders != null && cipher.Folders.Contains(cipher.UserId.Value.ToString()))
+ {
+ var folders = CoreHelpers.LoadClassFromJsonData>(cipher.Favorites);
+ folders.Remove(cipher.UserId.Value);
+ cipher.Favorites = JsonConvert.SerializeObject(folders);
+ }
+ }
+ var mappedEntity = Mapper.Map((TableModel.Cipher)cipher);
+ dbContext.Entry(entity).CurrentValues.SetValues(mappedEntity);
+ await UserBumpAccountRevisionDateByCipherId(cipher);
+ await dbContext.SaveChangesAsync();
+ }
+ }
+ }
+
+ public async Task ReplaceAsync(Cipher obj, IEnumerable collectionIds)
+ {
+ await UpdateCollections(obj, collectionIds);
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var cipher = await dbContext.Ciphers.FindAsync(obj.Id);
+ cipher.UserId = null;
+ cipher.OrganizationId = obj.OrganizationId;
+ cipher.Data = obj.Data;
+ cipher.Attachments = obj.Attachments;
+ cipher.RevisionDate = obj.RevisionDate;
+ cipher.DeletedDate = obj.DeletedDate;
+ await dbContext.SaveChangesAsync();
+
+ if (!string.IsNullOrWhiteSpace(cipher.Attachments))
+ {
+ if (cipher.OrganizationId.HasValue)
+ {
+ await OrganizationUpdateStorage(cipher.OrganizationId.Value);
+ }
+ else if (cipher.UserId.HasValue)
+ {
+ await UserUpdateStorage(cipher.UserId.Value);
+ }
+ }
+
+ await UserBumpAccountRevisionDateByCipherId(cipher);
+ return true;
+ }
+ }
+
+ public async Task RestoreAsync(IEnumerable ids, Guid userId)
+ {
+ return await ToggleCipherStates(ids, userId, CipherStateAction.Restore);
+ }
+
+ public async Task SoftDeleteAsync(IEnumerable ids, Guid userId)
+ {
+ await ToggleCipherStates(ids, userId, CipherStateAction.SoftDelete);
+ }
+
+ private async Task ToggleCipherStates(IEnumerable ids, Guid userId, CipherStateAction action)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var userCipherDetailsQuery = new UserCipherDetailsQuery(userId);
+ var cipherEntitiesToCheck = await (dbContext.Ciphers.Where(c => ids.Contains(c.Id))).ToListAsync();
+ var query = from ucd in await (userCipherDetailsQuery.Run(dbContext)).ToListAsync()
+ join c in cipherEntitiesToCheck
+ on ucd.Id equals c.Id
+ where ucd.Edit && ucd.DeletedDate == null
+ select c;
+
+ var utcNow = DateTime.UtcNow;
+ var cipherIdsToModify = query.Select(c => c.Id);
+ var cipherEntitiesToModify = dbContext.Ciphers.Where(x => cipherIdsToModify.Contains(x.Id));
+ if (action == CipherStateAction.HardDelete)
+ {
+ dbContext.RemoveRange(cipherEntitiesToModify);
+ }
+ else
+ {
+ await cipherEntitiesToModify.ForEachAsync(cipher =>
+ {
+ dbContext.Attach(cipher);
+ cipher.DeletedDate = action == CipherStateAction.Restore ? null : utcNow;
+ cipher.RevisionDate = utcNow;
+ });
+ }
+
+ var orgIds = query
+ .Where(c => c.OrganizationId.HasValue)
+ .GroupBy(c => c.OrganizationId).Select(x => x.Key);
+
+ foreach (var orgId in orgIds)
+ {
+ await OrganizationUpdateStorage(orgId.Value);
+ await UserBumpAccountRevisionDateByOrganizationId(orgId.Value);
+ }
+ if (query.Any(c => c.UserId.HasValue && !string.IsNullOrWhiteSpace(c.Attachments)))
+ {
+ await UserUpdateStorage(userId);
+ }
+ await UserBumpAccountRevisionDate(userId);
+ await dbContext.SaveChangesAsync();
+ return utcNow;
+ }
+ }
+
+ public async Task SoftDeleteByIdsOrganizationIdAsync(IEnumerable ids, Guid organizationId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var utcNow = DateTime.UtcNow;
+ var ciphers = dbContext.Ciphers.Where(c => ids.Contains(c.Id) && c.OrganizationId == organizationId);
+ await ciphers.ForEachAsync(cipher =>
+ {
+ dbContext.Attach(cipher);
+ cipher.DeletedDate = utcNow;
+ cipher.RevisionDate = utcNow;
+ });
+ await dbContext.SaveChangesAsync();
+ await OrganizationUpdateStorage(organizationId);
+ await UserBumpAccountRevisionDateByOrganizationId(organizationId);
+ }
+ }
+
+ public async Task UpdateAttachmentAsync(CipherAttachment attachment)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var cipher = await dbContext.Ciphers.FindAsync(attachment.Id);
+ var attachmentsJson = string.IsNullOrWhiteSpace(cipher.Attachments) ? new JObject() : JObject.Parse(cipher.Attachments);
+ attachmentsJson.Add(attachment.AttachmentId, attachment.AttachmentData);
+ cipher.Attachments = JsonConvert.SerializeObject(attachmentsJson);
+ await dbContext.SaveChangesAsync();
+
+ if (attachment.OrganizationId.HasValue)
+ {
+ await OrganizationUpdateStorage(cipher.OrganizationId.Value);
+ await UserBumpAccountRevisionDateByCipherId(new List { cipher });
+ }
+ else if (attachment.UserId.HasValue)
+ {
+ await UserUpdateStorage(attachment.UserId.Value);
+ await UserBumpAccountRevisionDate(attachment.UserId.Value);
+ }
+ }
+ }
+
+ public async Task UpdateCiphersAsync(Guid userId, IEnumerable ciphers)
+ {
+ if (!ciphers.Any())
+ {
+ return;
+ }
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var entities = Mapper.Map>(ciphers);
+ await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, entities);
+ await UserBumpAccountRevisionDate(userId);
+ }
+ }
+
+ public async Task UpdatePartialAsync(Guid id, Guid userId, Guid? folderId, bool favorite)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var cipher = await dbContext.Ciphers.FindAsync(id);
+
+ var foldersJson = JObject.Parse(cipher.Folders);
+ if (foldersJson == null && folderId.HasValue)
+ {
+ foldersJson.Add(userId.ToString(), folderId.Value);
+ }
+ else if (foldersJson != null && folderId.HasValue)
+ {
+ foldersJson[userId] = folderId.Value;
+ }
+ else
+ {
+ foldersJson.Remove(userId.ToString());
+ }
+
+ var favoritesJson = JObject.Parse(cipher.Favorites);
+ if (favorite)
+ {
+ favoritesJson.Add(userId.ToString(), favorite);
+ }
+ else
+ {
+ favoritesJson.Remove(userId.ToString());
+ }
+
+ await dbContext.SaveChangesAsync();
+ await UserBumpAccountRevisionDate(userId);
+ }
+ }
+
+ public async Task UpdateUserKeysAndCiphersAsync(User user, IEnumerable ciphers, IEnumerable folders, IEnumerable sends)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ await UserUpdateKeys(user);
+ var cipherEntities = Mapper.Map>(ciphers);
+ await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, cipherEntities);
+ var folderEntities = Mapper.Map>(folders);
+ await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, folderEntities);
+ var sendEntities = Mapper.Map>(sends);
+ await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, sendEntities);
+ await dbContext.SaveChangesAsync();
+ }
+ }
+
+ public async Task UpsertAsync(CipherDetails cipher)
+ {
+ if (cipher.Id.Equals(default))
+ {
+ await CreateAsync(cipher);
+ }
+ else
+ {
+ await ReplaceAsync(cipher);
+ }
+ }
+ }
+}
diff --git a/src/Core/Repositories/EntityFramework/CollectionCipherRepository.cs b/src/Core/Repositories/EntityFramework/CollectionCipherRepository.cs
new file mode 100644
index 0000000000..e1e788d69f
--- /dev/null
+++ b/src/Core/Repositories/EntityFramework/CollectionCipherRepository.cs
@@ -0,0 +1,242 @@
+using AutoMapper;
+using Bit.Core.Enums;
+using Bit.Core.Models.Table;
+using Bit.Core.Repositories.EntityFramework.Queries;
+using EfModel = Bit.Core.Models.EntityFramework;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using System;
+
+namespace Bit.Core.Repositories.EntityFramework
+{
+ public class CollectionCipherRepository : BaseEntityFrameworkRepository, ICollectionCipherRepository
+ {
+ public CollectionCipherRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
+ : base(serviceScopeFactory, mapper)
+ { }
+
+ public async Task CreateAsync(CollectionCipher obj)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var entity = Mapper.Map(obj);
+ dbContext.Add(entity);
+ await dbContext.SaveChangesAsync();
+ var organizationId = (await dbContext.Ciphers.FirstOrDefaultAsync(c => c.Id.Equals(obj.CipherId))).OrganizationId;
+ if (organizationId.HasValue)
+ {
+ await UserBumpAccountRevisionDateByCollectionId(obj.CollectionId, organizationId.Value);
+ }
+ return obj;
+ }
+ }
+
+ public async Task> GetManyByOrganizationIdAsync(Guid organizationId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var data = await (from cc in dbContext.CollectionCiphers
+ join c in dbContext.Collections
+ on cc.CollectionId equals c.Id
+ where c.OrganizationId == organizationId
+ select cc).ToArrayAsync();
+ return data;
+ }
+ }
+
+ public async Task> GetManyByUserIdAsync(Guid userId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var data = await new CollectionCipherReadByUserIdQuery(userId)
+ .Run(dbContext)
+ .ToArrayAsync();
+ return data;
+ }
+ }
+
+ public async Task> GetManyByUserIdCipherIdAsync(Guid userId, Guid cipherId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var data = await new CollectionCipherReadByUserIdCipherIdQuery(userId, cipherId)
+ .Run(dbContext)
+ .ToArrayAsync();
+ return data;
+ }
+ }
+
+ public async Task UpdateCollectionsAsync(Guid cipherId, Guid userId, IEnumerable collectionIds)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var organizationId = (await dbContext.Ciphers.FindAsync(cipherId)).OrganizationId;
+ var availableCollectionsCte = from c in dbContext.Collections
+ join o in dbContext.Organizations
+ on c.OrganizationId equals o.Id
+ join ou in dbContext.OrganizationUsers
+ on o.Id equals ou.OrganizationId
+ where ou.UserId == userId
+ join cu in dbContext.CollectionUsers
+ on ou.Id equals cu.OrganizationUserId into cu_g
+ from cu in cu_g.DefaultIfEmpty()
+ where !ou.AccessAll && cu.CollectionId == c.Id
+ join gu in dbContext.GroupUsers
+ on ou.Id equals gu.OrganizationUserId into gu_g
+ from gu in gu_g.DefaultIfEmpty()
+ where cu.CollectionId == null && !ou.AccessAll
+ join g in dbContext.Groups
+ on gu.GroupId equals g.Id into g_g
+ from g in g_g.DefaultIfEmpty()
+ join cg in dbContext.CollectionGroups
+ on gu.GroupId equals cg.GroupId into cg_g
+ from cg in cg_g.DefaultIfEmpty()
+ where !g.AccessAll && cg.CollectionId == c.Id &&
+ (o.Id == organizationId && o.Enabled && ou.Status == OrganizationUserStatusType.Confirmed && (
+ ou.AccessAll || !cu.ReadOnly || g.AccessAll || !cg.ReadOnly))
+ select new { c, o, cu, gu, g, cg };
+ var target = from cc in dbContext.CollectionCiphers
+ where cc.CipherId == cipherId
+ select new { cc.CollectionId, cc.CipherId };
+ var source = collectionIds.Select(x => new { CollectionId = x, CipherId = cipherId });
+ var merge1 = from t in target
+ join s in source
+ on t.CollectionId equals s.CollectionId into s_g
+ from s in s_g.DefaultIfEmpty()
+ where t.CipherId == s.CipherId
+ select new { t, s };
+ var merge2 = from s in source
+ join t in target
+ on s.CollectionId equals t.CollectionId into t_g
+ from t in t_g.DefaultIfEmpty()
+ where t.CipherId == s.CipherId
+ select new { t, s };
+ var union = merge1.Union(merge2).Distinct();
+ var insert = union
+ .Where(x => x.t == null && collectionIds.Contains(x.s.CollectionId))
+ .Select(x => new EfModel.CollectionCipher
+ {
+ CollectionId = x.s.CollectionId,
+ CipherId = x.s.CipherId,
+ });
+ var delete = union
+ .Where(x => x.s == null && x.t.CipherId == cipherId && collectionIds.Contains(x.t.CollectionId))
+ .Select(x => new EfModel.CollectionCipher
+ {
+ CollectionId = x.t.CollectionId,
+ CipherId = x.t.CipherId,
+ });
+ await dbContext.AddRangeAsync(insert);
+ dbContext.RemoveRange(delete);
+ await dbContext.SaveChangesAsync();
+
+ if (organizationId.HasValue)
+ {
+ await UserBumpAccountRevisionDateByOrganizationId(organizationId.Value);
+ }
+ }
+ }
+
+ public async Task UpdateCollectionsForAdminAsync(Guid cipherId, Guid organizationId, IEnumerable collectionIds)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var availableCollectionsCte = from c in dbContext.Collections
+ where c.OrganizationId == organizationId
+ select c;
+ var target = from cc in dbContext.CollectionCiphers
+ where cc.CipherId == cipherId
+ select new { cc.CollectionId, cc.CipherId };
+ var source = collectionIds.Select(x => new { CollectionId = x, CipherId = cipherId });
+ var merge1 = from t in target
+ join s in source
+ on t.CollectionId equals s.CollectionId into s_g
+ from s in s_g.DefaultIfEmpty()
+ where t.CipherId == s.CipherId
+ select new { t, s };
+ var merge2 = from s in source
+ join t in target
+ on s.CollectionId equals t.CollectionId into t_g
+ from t in t_g.DefaultIfEmpty()
+ where t.CipherId == s.CipherId
+ select new { t, s };
+ var union = merge1.Union(merge2).Distinct();
+ var insert = union
+ .Where(x => x.t == null && collectionIds.Contains(x.s.CollectionId))
+ .Select(x => new EfModel.CollectionCipher
+ {
+ CollectionId = x.s.CollectionId,
+ CipherId = x.s.CipherId,
+ });
+ var delete = union
+ .Where(x => x.s == null && x.t.CipherId == cipherId)
+ .Select(x => new EfModel.CollectionCipher
+ {
+ CollectionId = x.t.CollectionId,
+ CipherId = x.t.CipherId,
+ });
+ await dbContext.AddRangeAsync(insert);
+ dbContext.RemoveRange(delete);
+ await dbContext.SaveChangesAsync();
+ await UserBumpAccountRevisionDateByOrganizationId(organizationId);
+ }
+ }
+
+ public async Task UpdateCollectionsForCiphersAsync(IEnumerable cipherIds, Guid userId, Guid organizationId, IEnumerable collectionIds)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var availibleCollections = from c in dbContext.Collections
+ join o in dbContext.Organizations
+ on c.OrganizationId equals o.Id
+ join ou in dbContext.OrganizationUsers
+ on o.Id equals ou.OrganizationId
+ where ou.UserId == userId
+ join cu in dbContext.CollectionUsers
+ on ou.Id equals cu.OrganizationUserId into cu_g
+ from cu in cu_g.DefaultIfEmpty()
+ where !ou.AccessAll && cu.CollectionId == c.Id
+ join gu in dbContext.GroupUsers
+ on ou.Id equals gu.OrganizationUserId into gu_g
+ from gu in gu_g.DefaultIfEmpty()
+ where cu.CollectionId == null && !ou.AccessAll
+ join g in dbContext.Groups
+ on gu.GroupId equals g.Id into g_g
+ from g in g_g.DefaultIfEmpty()
+ join cg in dbContext.CollectionGroups
+ on gu.GroupId equals cg.GroupId into cg_g
+ from cg in cg_g.DefaultIfEmpty()
+ where !g.AccessAll && cg.CollectionId == c.Id &&
+ (o.Id == organizationId && o.Enabled && ou.Status == OrganizationUserStatusType.Confirmed &&
+ (ou.AccessAll || !cu.ReadOnly || g.AccessAll || !cg.ReadOnly))
+ select new { c, o, ou, cu, gu, g, cg };
+ var count = await availibleCollections.CountAsync();
+ if (await availibleCollections.CountAsync() < 1)
+ {
+ return;
+ }
+
+ var insertData = from collectionId in collectionIds
+ from cipherId in cipherIds
+ where availibleCollections.Select(x => x.c.Id).Contains(collectionId)
+ select new EfModel.CollectionCipher
+ {
+ CollectionId = collectionId,
+ CipherId = cipherId,
+ };
+ await dbContext.AddRangeAsync(insertData);
+ await UserBumpAccountRevisionDateByOrganizationId(organizationId);
+ }
+ }
+ }
+}
diff --git a/src/Core/Repositories/EntityFramework/CollectionRepository.cs b/src/Core/Repositories/EntityFramework/CollectionRepository.cs
new file mode 100644
index 0000000000..33823bd3e0
--- /dev/null
+++ b/src/Core/Repositories/EntityFramework/CollectionRepository.cs
@@ -0,0 +1,247 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using AutoMapper;
+using Bit.Core.Models.Data;
+using Bit.Core.Models.Table;
+using Bit.Core.Repositories.EntityFramework.Queries;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using EfModel = Bit.Core.Models.EntityFramework;
+using TableModel = Bit.Core.Models.Table;
+
+namespace Bit.Core.Repositories.EntityFramework
+{
+ public class CollectionRepository : Repository, ICollectionRepository
+ {
+ public CollectionRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
+ : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.Collections)
+ { }
+
+ public override async Task CreateAsync(Collection obj)
+ {
+ await base.CreateAsync(obj);
+ await UserBumpAccountRevisionDateByCollectionId(obj.Id, obj.OrganizationId);
+ return obj;
+ }
+
+ public async Task CreateAsync(Collection obj, IEnumerable groups)
+ {
+ await base.CreateAsync(obj);
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var availibleGroups = await (from g in dbContext.Groups
+ where g.OrganizationId == obj.OrganizationId
+ select g.Id).ToListAsync();
+ var collectionGroups = groups
+ .Where(g => availibleGroups.Contains(g.Id))
+ .Select(g => new EfModel.CollectionGroup
+ {
+ CollectionId = obj.Id,
+ GroupId = g.Id,
+ ReadOnly = g.ReadOnly,
+ HidePasswords = g.HidePasswords,
+ });
+ await dbContext.AddRangeAsync(collectionGroups);
+ await dbContext.SaveChangesAsync();
+ await UserBumpAccountRevisionDateByOrganizationId(obj.OrganizationId);
+ }
+ }
+
+ public async Task DeleteUserAsync(Guid collectionId, Guid organizationUserId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = from cu in dbContext.CollectionUsers
+ where cu.CollectionId == collectionId &&
+ cu.OrganizationUserId == organizationUserId
+ select cu;
+ dbContext.RemoveRange(await query.ToListAsync());
+ await dbContext.SaveChangesAsync();
+ await UserBumpAccountRevisionDateByOrganizationUserId(organizationUserId);
+ }
+ }
+
+ public async Task GetByIdAsync(Guid id, Guid userId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = new UserCollectionDetailsQuery(userId);
+ var collection = await query.Run(dbContext).FirstOrDefaultAsync();
+ return collection;
+ }
+ }
+
+ public async Task>> GetByIdWithGroupsAsync(Guid id)
+ {
+ var collection = await base.GetByIdAsync(id);
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var collectionGroups = await (from cg in dbContext.CollectionGroups
+ where cg.CollectionId == id
+ select cg).ToListAsync();
+ var selectionReadOnlys = collectionGroups.Select(cg => new SelectionReadOnly
+ {
+ Id = cg.GroupId,
+ ReadOnly = cg.ReadOnly,
+ HidePasswords = cg.HidePasswords,
+ }).ToList();
+ return new Tuple>(collection, selectionReadOnlys);
+ }
+ }
+
+ public async Task>> GetByIdWithGroupsAsync(Guid id, Guid userId)
+ {
+ var collection = await GetByIdAsync(id, userId);
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = from cg in dbContext.CollectionGroups
+ where cg.CollectionId.Equals(id)
+ select new SelectionReadOnly
+ {
+ Id = cg.GroupId,
+ ReadOnly = cg.ReadOnly,
+ HidePasswords = cg.HidePasswords,
+ };
+ var configurations = await query.ToArrayAsync();
+ return new Tuple>(collection, configurations);
+ }
+ }
+
+ public async Task GetCountByOrganizationIdAsync(Guid organizationId)
+ {
+ var query = new CollectionReadCountByOrganizationIdQuery(organizationId);
+ return await GetCountFromQuery(query);
+ }
+
+ public async Task> GetManyByOrganizationIdAsync(Guid organizationId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = from c in dbContext.Collections
+ where c.OrganizationId == organizationId
+ select c;
+ var collections = await query.ToArrayAsync();
+ return collections;
+ }
+ }
+
+ public async Task> GetManyByUserIdAsync(Guid userId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = new UserCollectionDetailsQuery(userId).Run(dbContext);
+ var data = await query.ToListAsync();
+ return data.GroupBy(c => c.Id).Select(c => c.First()).ToList();
+ }
+ }
+
+ public async Task> GetManyUsersByIdAsync(Guid id)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = from cu in dbContext.CollectionUsers
+ where cu.CollectionId == id
+ select cu;
+ var collectionUsers = await query.ToListAsync();
+ return collectionUsers.Select(cu => new SelectionReadOnly
+ {
+ Id = cu.OrganizationUserId,
+ ReadOnly = cu.ReadOnly,
+ HidePasswords = cu.HidePasswords,
+ }).ToArray();
+ }
+ }
+
+ public async Task ReplaceAsync(Collection collection, IEnumerable groups)
+ {
+ await base.ReplaceAsync(collection);
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var groupsInOrg = dbContext.Groups.Where(g => g.OrganizationId == collection.OrganizationId);
+ var modifiedGroupEntities = dbContext.Groups.Where(x => groups.Select(x => x.Id).Contains(x.Id));
+ var target = (from cg in dbContext.CollectionGroups
+ join g in modifiedGroupEntities
+ on cg.CollectionId equals collection.Id into s_g
+ from g in s_g.DefaultIfEmpty()
+ where g == null || cg.GroupId == g.Id
+ select new {cg, g}).AsNoTracking();
+ var source = (from g in modifiedGroupEntities
+ from cg in dbContext.CollectionGroups
+ .Where(cg => cg.CollectionId == collection.Id && cg.GroupId == g.Id).DefaultIfEmpty()
+ select new {cg, g}).AsNoTracking();
+ var union = await target
+ .Union(source)
+ .Where(x =>
+ x.cg == null ||
+ ((x.g == null || x.g.Id == x.cg.GroupId) &&
+ (x.cg.CollectionId == collection.Id)))
+ .AsNoTracking()
+ .ToListAsync();
+ var insert = union.Where(x => x.cg == null && groupsInOrg.Any(c => x.g.Id == c.Id))
+ .Select(x => new EfModel.CollectionGroup
+ {
+ CollectionId = collection.Id,
+ GroupId = x.g.Id,
+ ReadOnly = groups.FirstOrDefault(g => g.Id == x.g.Id).ReadOnly,
+ HidePasswords = groups.FirstOrDefault(g => g.Id == x.g.Id).HidePasswords,
+ }).ToList();
+ var update = union
+ .Where(
+ x => x.g != null &&
+ x.cg != null &&
+ (x.cg.ReadOnly != groups.FirstOrDefault(g => g.Id == x.g.Id).ReadOnly ||
+ x.cg.HidePasswords != groups.FirstOrDefault(g => g.Id == x.g.Id).HidePasswords)
+ )
+ .Select(x => new EfModel.CollectionGroup
+ {
+ CollectionId = collection.Id,
+ GroupId = x.g.Id,
+ ReadOnly = groups.FirstOrDefault(g => g.Id == x.g.Id).ReadOnly,
+ HidePasswords = groups.FirstOrDefault(g => g.Id == x.g.Id).HidePasswords,
+ });
+ var delete = union
+ .Where(
+ x => x.g == null &&
+ x.cg.CollectionId == collection.Id
+ )
+ .Select(x => new EfModel.CollectionGroup
+ {
+ CollectionId = collection.Id,
+ GroupId = x.cg.GroupId,
+ })
+ .ToList();
+
+ await dbContext.AddRangeAsync(insert);
+ dbContext.UpdateRange(update);
+ dbContext.RemoveRange(delete);
+ await dbContext.SaveChangesAsync();
+ await UserBumpAccountRevisionDateByCollectionId(collection.Id, collection.OrganizationId);
+ }
+ }
+
+ public async Task UpdateUsersAsync(Guid id, IEnumerable users)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var procedure = new CollectionUserUpdateUsersQuery(id, users);
+ var updateData = await procedure.Update.BuildInMemory(dbContext);
+ dbContext.UpdateRange(updateData);
+ var insertData = await procedure.Insert.BuildInMemory(dbContext);
+ await dbContext.AddRangeAsync(insertData);
+ dbContext.RemoveRange(await procedure.Delete.Run(dbContext).ToListAsync());
+ }
+ }
+ }
+}
diff --git a/src/Core/Repositories/EntityFramework/DatabaseContext.cs b/src/Core/Repositories/EntityFramework/DatabaseContext.cs
index df418a0ee0..c0940d330b 100644
--- a/src/Core/Repositories/EntityFramework/DatabaseContext.cs
+++ b/src/Core/Repositories/EntityFramework/DatabaseContext.cs
@@ -1,39 +1,136 @@
-using System;
-using Bit.Core.Models.EntityFramework;
+using Bit.Core.Models.EntityFramework;
+using Bit.Core.Models.EntityFramework.Provider;
using Microsoft.EntityFrameworkCore;
namespace Bit.Core.Repositories.EntityFramework
{
public class DatabaseContext : DbContext
{
+ public const string postgresIndetermanisticCollation = "postgresIndetermanisticCollation";
+
public DatabaseContext(DbContextOptions options)
: base(options)
{ }
- public DbSet Users { get; set; }
public DbSet Ciphers { get; set; }
+ public DbSet Collections { get; set; }
+ public DbSet CollectionCiphers { get; set; }
+ public DbSet CollectionGroups { get; set; }
+ public DbSet CollectionUsers { get; set; }
+ public DbSet Devices { get; set; }
+ public DbSet EmergencyAccesses { get; set; }
+ public DbSet Events { get; set; }
+ public DbSet Folders { get; set; }
+ public DbSet Grants { get; set; }
+ public DbSet Groups { get; set; }
+ public DbSet GroupUsers { get; set; }
+ public DbSet Installations { get; set; }
public DbSet Organizations { get; set; }
-
+ public DbSet OrganizationUsers { get; set; }
+ public DbSet Policies { get; set; }
+ public DbSet Providers { get; set; }
+ public DbSet ProviderUsers { get; set; }
+ public DbSet ProviderOrganizations { get; set; }
+ public DbSet ProviderOrganizationProviderUsers { get; set; }
+ public DbSet Sends { get; set; }
+ public DbSet SsoConfigs { get; set; }
+ public DbSet SsoUsers { get; set; }
+ public DbSet TaxRates { get; set; }
+ public DbSet Transactions { get; set; }
+ public DbSet U2fs { get; set; }
+ public DbSet Users { get; set; }
+
protected override void OnModelCreating(ModelBuilder builder)
{
- builder.Entity().Ignore(e => e.Data);
- builder.Entity().Property(e => e.DataJson).HasColumnName("Data");
- builder.Entity().Ignore(e => e.Attachments);
- builder.Entity().Property(e => e.AttachmentsJson).HasColumnName("Attachments");
- builder.Entity().Ignore(e => e.Favorites);
- builder.Entity().Property(e => e.FavoritesJson).HasColumnName("Favorites");
- builder.Entity().Ignore(e => e.Folders);
- builder.Entity().Property(e => e.FoldersJson).HasColumnName("Folders");
+ var eCipher = builder.Entity();
+ var eCollection = builder.Entity();
+ var eCollectionCipher = builder.Entity();
+ var eCollectionUser = builder.Entity();
+ var eCollectionGroup = builder.Entity();
+ var eDevice = builder.Entity();
+ var eEmergencyAccess = builder.Entity();
+ var eEvent = builder.Entity();
+ var eFolder = builder.Entity();
+ var eGrant = builder.Entity();
+ var eGroup = builder.Entity();
+ var eGroupUser = builder.Entity();
+ var eInstallation = builder.Entity();
+ var eOrganization = builder.Entity();
+ var eOrganizationUser = builder.Entity();
+ var ePolicy = builder.Entity();
+ var eProvider = builder.Entity();
+ var eProviderUser = builder.Entity();
+ var eProviderOrganization = builder.Entity();
+ var eProviderOrganizationProviderUser = builder.Entity();
+ var eSend = builder.Entity();
+ var eSsoConfig = builder.Entity();
+ var eSsoUser = builder.Entity();
+ var eTaxRate = builder.Entity();
+ var eTransaction = builder.Entity();
+ var eU2f = builder.Entity();
+ var eUser = builder.Entity();
- builder.Entity().Ignore(e => e.TwoFactorProviders);
- builder.Entity().Property(e => e.TwoFactorProvidersJson).HasColumnName("TwoFactorProviders");
+ eCipher.Property(c => c.Id).ValueGeneratedNever();
+ eCollection.Property(c => c.Id).ValueGeneratedNever();
+ eEmergencyAccess.Property(c => c.Id).ValueGeneratedNever();
+ eEvent.Property(c => c.Id).ValueGeneratedNever();
+ eFolder.Property(c => c.Id).ValueGeneratedNever();
+ eGroup.Property(c => c.Id).ValueGeneratedNever();
+ eInstallation.Property(c => c.Id).ValueGeneratedNever();
+ eOrganization.Property(c => c.Id).ValueGeneratedNever();
+ eOrganizationUser.Property(c => c.Id).ValueGeneratedNever();
+ ePolicy.Property(c => c.Id).ValueGeneratedNever();
+ eProvider.Property(c => c.Id).ValueGeneratedNever();
+ eProviderUser.Property(c => c.Id).ValueGeneratedNever();
+ eProviderOrganization.Property(c => c.Id).ValueGeneratedNever();
+ eProviderOrganizationProviderUser.Property(c => c.Id).ValueGeneratedNever();
+ eSend.Property(c => c.Id).ValueGeneratedNever();
+ eTransaction.Property(c => c.Id).ValueGeneratedNever();
+ eUser.Property(c => c.Id).ValueGeneratedNever();
- builder.Entity().Ignore(e => e.TwoFactorProviders);
- builder.Entity().Property(e => e.TwoFactorProvidersJson).HasColumnName("TwoFactorProviders");
+ eCollectionCipher.HasKey(cc => new { cc.CollectionId, cc.CipherId });
+ eCollectionUser.HasKey(cu => new { cu.CollectionId, cu.OrganizationUserId });
+ eCollectionGroup.HasKey(cg => new { cg.CollectionId, cg.GroupId });
+ eGrant.HasKey(x => x.Key);
+ eGroupUser.HasKey(gu => new { gu.GroupId, gu.OrganizationUserId });
- builder.Entity().ToTable(nameof(User));
- builder.Entity().ToTable(nameof(Cipher));
- builder.Entity().ToTable(nameof(Organization));
+
+ if (Database.IsNpgsql())
+ {
+ // the postgres provider doesn't currently support database level non-deterministic collations.
+ // see https://www.npgsql.org/efcore/misc/collations-and-case-sensitivity.html#database-collation
+ builder.HasCollation(postgresIndetermanisticCollation, locale: "en-u-ks-primary", provider: "icu", deterministic: false);
+ eUser.Property(e => e.Email).UseCollation(postgresIndetermanisticCollation);
+ eSsoUser.Property(e => e.ExternalId).UseCollation(postgresIndetermanisticCollation);
+ eOrganization.Property(e => e.Identifier).UseCollation(postgresIndetermanisticCollation);
+ //
+ }
+
+ eCipher.ToTable(nameof(Cipher));
+ eCollection.ToTable(nameof(Collection));
+ eCollectionCipher.ToTable(nameof(CollectionCipher));
+ eDevice.ToTable(nameof(Device));
+ eEmergencyAccess.ToTable(nameof(EmergencyAccess));
+ eEvent.ToTable(nameof(Event));
+ eFolder.ToTable(nameof(Folder));
+ eGrant.ToTable(nameof(Grant));
+ eGroup.ToTable(nameof(Group));
+ eGroupUser.ToTable(nameof(GroupUser));
+ eInstallation.ToTable(nameof(Installation));
+ eOrganization.ToTable(nameof(Organization));
+ eOrganizationUser.ToTable(nameof(OrganizationUser));
+ ePolicy.ToTable(nameof(Policy));
+ eProvider.ToTable(nameof(Provider));
+ eProviderUser.ToTable(nameof(ProviderUser));
+ eProviderOrganization.ToTable(nameof(ProviderOrganization));
+ eProviderOrganizationProviderUser.ToTable(nameof(ProviderOrganizationProviderUser));
+ eSend.ToTable(nameof(Send));
+ eSsoConfig.ToTable(nameof(SsoConfig));
+ eSsoUser.ToTable(nameof(SsoUser));
+ eTaxRate.ToTable(nameof(TaxRate));
+ eTransaction.ToTable(nameof(Transaction));
+ eU2f.ToTable(nameof(U2f));
+ eUser.ToTable(nameof(User));
}
}
}
diff --git a/src/Core/Repositories/EntityFramework/DeviceRepository.cs b/src/Core/Repositories/EntityFramework/DeviceRepository.cs
new file mode 100644
index 0000000000..79f6e8c93f
--- /dev/null
+++ b/src/Core/Repositories/EntityFramework/DeviceRepository.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using AutoMapper;
+using Bit.Core.Models.Table;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using EfModel = Bit.Core.Models.EntityFramework;
+using TableModel = Bit.Core.Models.Table;
+
+namespace Bit.Core.Repositories.EntityFramework
+{
+ public class DeviceRepository : Repository, IDeviceRepository
+ {
+ public DeviceRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
+ : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.Devices)
+ { }
+
+ public async Task ClearPushTokenAsync(Guid id)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = dbContext.Devices.Where(d => d.Id == id);
+ dbContext.AttachRange(query);
+ await query.ForEachAsync(x => x.PushToken = null);
+ await dbContext.SaveChangesAsync();
+ }
+ }
+
+ public async Task GetByIdAsync(Guid id, Guid userId)
+ {
+ var device = await base.GetByIdAsync(id);
+ if (device == null || device.UserId != userId)
+ {
+ return null;
+ }
+
+ return Mapper.Map(device);
+ }
+
+ public async Task GetByIdentifierAsync(string identifier)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = dbContext.Devices.Where(d => d.Identifier == identifier);
+ var device = await query.FirstOrDefaultAsync();
+ return Mapper.Map(device);
+ }
+ }
+
+ public async Task GetByIdentifierAsync(string identifier, Guid userId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = dbContext.Devices.Where(d => d.Identifier == identifier && d.UserId == userId);
+ var device = await query.FirstOrDefaultAsync();
+ return Mapper.Map(device);
+ }
+ }
+
+ public async Task> GetManyByUserIdAsync(Guid userId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = dbContext.Devices.Where(d => d.UserId == userId);
+ var devices = await query.ToListAsync();
+ return Mapper.Map>(devices);
+ }
+ }
+ }
+}
diff --git a/src/Core/Repositories/EntityFramework/EmergencyAccessRepository.cs b/src/Core/Repositories/EntityFramework/EmergencyAccessRepository.cs
new file mode 100644
index 0000000000..c8c4ecfaa4
--- /dev/null
+++ b/src/Core/Repositories/EntityFramework/EmergencyAccessRepository.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using AutoMapper;
+using Bit.Core.Enums;
+using Bit.Core.Models.Data;
+using Bit.Core.Repositories.EntityFramework.Queries;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using EfModel = Bit.Core.Models.EntityFramework;
+using TableModel = Bit.Core.Models.Table;
+
+namespace Bit.Core.Repositories.EntityFramework
+{
+ public class EmergencyAccessRepository : Repository, IEmergencyAccessRepository
+ {
+ public EmergencyAccessRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
+ : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.EmergencyAccesses)
+ { }
+
+ public async Task GetCountByGrantorIdEmailAsync(Guid grantorId, string email, bool onlyRegisteredUsers)
+ {
+ var query = new EmergencyAccessReadCountByGrantorIdEmailQuery(grantorId, email, onlyRegisteredUsers);
+ return await GetCountFromQuery(query);
+ }
+
+ public async Task GetDetailsByIdGrantorIdAsync(Guid id, Guid grantorId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var view = new EmergencyAccessDetailsViewQuery();
+ var query = view.Run(dbContext).Where(ea =>
+ ea.Id == id &&
+ ea.GrantorId == grantorId
+ );
+ return await query.FirstOrDefaultAsync();
+ }
+ }
+
+ public async Task> GetExpiredRecoveriesAsync()
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var view = new EmergencyAccessDetailsViewQuery();
+ var query = view.Run(dbContext).Where(ea =>
+ ea.Status == EmergencyAccessStatusType.RecoveryInitiated
+ );
+ return await query.ToListAsync();
+ }
+ }
+
+ public async Task> GetManyDetailsByGranteeIdAsync(Guid granteeId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var view = new EmergencyAccessDetailsViewQuery();
+ var query = view.Run(dbContext).Where(ea =>
+ ea.GranteeId == granteeId
+ );
+ return await query.ToListAsync();
+ }
+ }
+
+ public async Task> GetManyDetailsByGrantorIdAsync(Guid grantorId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var view = new EmergencyAccessDetailsViewQuery();
+ var query = view.Run(dbContext).Where(ea =>
+ ea.GrantorId == grantorId
+ );
+ return await query.ToListAsync();
+ }
+ }
+
+ public async Task> GetManyToNotifyAsync()
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var view = new EmergencyAccessDetailsViewQuery();
+ var query = view.Run(dbContext).Where(ea =>
+ ea.Status == EmergencyAccessStatusType.RecoveryInitiated
+ );
+ var notifies = await query.Select(ea => new EmergencyAccessNotify
+ {
+ Id = ea.Id,
+ GrantorId = ea.GrantorId,
+ GranteeId = ea.GranteeId,
+ Email = ea.Email,
+ KeyEncrypted = ea.KeyEncrypted,
+ Type = ea.Type,
+ Status = ea.Status,
+ WaitTimeDays = ea.WaitTimeDays,
+ RecoveryInitiatedDate = ea.RecoveryInitiatedDate,
+ LastNotificationDate = ea.LastNotificationDate,
+ CreationDate = ea.CreationDate,
+ RevisionDate = ea.RevisionDate,
+ GranteeName = ea.GranteeName,
+ GranteeEmail = ea.GranteeEmail,
+ GrantorEmail = ea.GrantorEmail,
+ }).ToListAsync();
+ return notifies;
+ }
+ }
+ }
+}
diff --git a/src/Core/Repositories/EntityFramework/EventRepository.cs b/src/Core/Repositories/EntityFramework/EventRepository.cs
new file mode 100644
index 0000000000..bff7843b87
--- /dev/null
+++ b/src/Core/Repositories/EntityFramework/EventRepository.cs
@@ -0,0 +1,156 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using AutoMapper;
+using Bit.Core.Models.Data;
+using Bit.Core.Models.Table;
+using Bit.Core.Repositories.EntityFramework.Queries;
+using LinqToDB.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using DataModel = Bit.Core.Models.Data;
+using EfModel = Bit.Core.Models.EntityFramework;
+using TableModel = Bit.Core.Models.Table;
+
+namespace Bit.Core.Repositories.EntityFramework
+{
+ public class EventRepository : Repository, IEventRepository
+ {
+ public EventRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
+ : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.Events)
+ { }
+
+ public async Task CreateAsync(IEvent e)
+ {
+ if (e is not Event ev)
+ {
+ ev = new Event(e);
+ }
+
+ await base.CreateAsync(ev);
+ }
+
+ public async Task CreateManyAsync(IEnumerable entities)
+ {
+ if (!entities?.Any() ?? true)
+ {
+ return;
+ }
+
+ if (!entities.Skip(1).Any())
+ {
+ await CreateAsync(entities.First());
+ return;
+ }
+
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var tableEvents = entities.Select(e => e as Event ?? new Event(e));
+ var entityEvents = Mapper.Map>(tableEvents);
+ await dbContext.BulkCopyAsync(entityEvents);
+ }
+ }
+
+ public async Task> GetManyByCipherAsync(Cipher cipher, DateTime startDate, DateTime endDate, PageOptions pageOptions)
+ {
+ DateTime? beforeDate = null;
+ if (!string.IsNullOrWhiteSpace(pageOptions.ContinuationToken) &&
+ long.TryParse(pageOptions.ContinuationToken, out var binaryDate))
+ {
+ beforeDate = DateTime.SpecifyKind(DateTime.FromBinary(binaryDate), DateTimeKind.Utc);
+ }
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = new EventReadPageByCipherIdQuery(cipher, startDate, endDate, beforeDate, pageOptions);
+ var events = await query.Run(dbContext).ToListAsync();
+
+ var result = new PagedResult();
+ if (events.Any() && events.Count >= pageOptions.PageSize)
+ {
+ result.ContinuationToken = events.Last().Date.ToBinary().ToString();
+ }
+ result.Data.AddRange(events);
+ return result;
+ }
+ }
+
+
+ public async Task> GetManyByOrganizationActingUserAsync(Guid organizationId, Guid actingUserId, DateTime startDate, DateTime endDate, PageOptions pageOptions)
+ {
+ DateTime? beforeDate = null;
+ if (!string.IsNullOrWhiteSpace(pageOptions.ContinuationToken) &&
+ long.TryParse(pageOptions.ContinuationToken, out var binaryDate))
+ {
+ beforeDate = DateTime.SpecifyKind(DateTime.FromBinary(binaryDate), DateTimeKind.Utc);
+ }
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = new EventReadPageByOrganizationIdActingUserIdQuery(organizationId, actingUserId,
+ startDate, endDate, beforeDate, pageOptions);
+ var events = await query.Run(dbContext).ToListAsync();
+
+ var result = new PagedResult();
+ if (events.Any() && events.Count >= pageOptions.PageSize)
+ {
+ result.ContinuationToken = events.Last().Date.ToBinary().ToString();
+ }
+ result.Data.AddRange(events);
+ return result;
+ }
+ }
+
+ public async Task> GetManyByOrganizationAsync(Guid organizationId, DateTime startDate, DateTime endDate, PageOptions pageOptions)
+ {
+ DateTime? beforeDate = null;
+ if (!string.IsNullOrWhiteSpace(pageOptions.ContinuationToken) &&
+ long.TryParse(pageOptions.ContinuationToken, out var binaryDate))
+ {
+ beforeDate = DateTime.SpecifyKind(DateTime.FromBinary(binaryDate), DateTimeKind.Utc);
+ }
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = new EventReadPageByOrganizationIdQuery(organizationId, startDate,
+ endDate, beforeDate, pageOptions);
+ var events = await query.Run(dbContext).ToListAsync();
+
+ var result = new PagedResult();
+ if (events.Any() && events.Count >= pageOptions.PageSize)
+ {
+ result.ContinuationToken = events.Last().Date.ToBinary().ToString();
+ }
+ result.Data.AddRange(events);
+ return result;
+ }
+ }
+
+ public async Task> GetManyByUserAsync(Guid userId, DateTime startDate, DateTime endDate, PageOptions pageOptions)
+ {
+ DateTime? beforeDate = null;
+ if (!string.IsNullOrWhiteSpace(pageOptions.ContinuationToken) &&
+ long.TryParse(pageOptions.ContinuationToken, out var binaryDate))
+ {
+ beforeDate = DateTime.SpecifyKind(DateTime.FromBinary(binaryDate), DateTimeKind.Utc);
+ }
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = new EventReadPageByUserIdQuery(userId, startDate,
+ endDate, beforeDate, pageOptions);
+ var events = await query.Run(dbContext).ToListAsync();
+
+ var result = new PagedResult();
+ if (events.Any() && events.Count >= pageOptions.PageSize)
+ {
+ result.ContinuationToken = events.Last().Date.ToBinary().ToString();
+ }
+ result.Data.AddRange(events);
+ return result;
+ }
+ }
+ }
+}
diff --git a/src/Core/Repositories/EntityFramework/FolderRepository.cs b/src/Core/Repositories/EntityFramework/FolderRepository.cs
new file mode 100644
index 0000000000..823491f351
--- /dev/null
+++ b/src/Core/Repositories/EntityFramework/FolderRepository.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using AutoMapper;
+using Bit.Core.Models.Table;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using DataModel = Bit.Core.Models.Data;
+using EfModel = Bit.Core.Models.EntityFramework;
+using TableModel = Bit.Core.Models.Table;
+
+namespace Bit.Core.Repositories.EntityFramework
+{
+ public class FolderRepository : Repository, IFolderRepository
+ {
+ public FolderRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
+ : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.Folders)
+ { }
+
+ public async Task GetByIdAsync(Guid id, Guid userId)
+ {
+ var folder = await base.GetByIdAsync(id);
+ if (folder == null || folder.UserId != userId)
+ {
+ return null;
+ }
+
+ return folder;
+ }
+
+ public async Task> GetManyByUserIdAsync(Guid userId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = from f in dbContext.Folders
+ where f.UserId == userId
+ select f;
+ var folders = await query.ToListAsync();
+ return Mapper.Map>(folders);
+ }
+ }
+ }
+}
diff --git a/src/Core/Repositories/EntityFramework/GrantRepository.cs b/src/Core/Repositories/EntityFramework/GrantRepository.cs
new file mode 100644
index 0000000000..a71aff2272
--- /dev/null
+++ b/src/Core/Repositories/EntityFramework/GrantRepository.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using AutoMapper;
+using Bit.Core.Models.Table;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using DataModel = Bit.Core.Models.Data;
+using EfModel = Bit.Core.Models.EntityFramework;
+using TableModel = Bit.Core.Models.Table;
+
+namespace Bit.Core.Repositories.EntityFramework
+{
+ public class GrantRepository : BaseEntityFrameworkRepository, IGrantRepository
+ {
+ public GrantRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
+ : base(serviceScopeFactory, mapper)
+ { }
+
+ public async Task DeleteByKeyAsync(string key)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = from g in dbContext.Grants
+ where g.Key == key
+ select g;
+ dbContext.Remove(query);
+ await dbContext.SaveChangesAsync();
+ }
+ }
+
+ public async Task DeleteManyAsync(string subjectId, string sessionId, string clientId, string type)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = from g in dbContext.Grants
+ where g.SubjectId == subjectId &&
+ g.ClientId == clientId &&
+ g.SessionId == sessionId &&
+ g.Type == type
+ select g;
+ dbContext.Remove(query);
+ await dbContext.SaveChangesAsync();
+ }
+ }
+
+ public async Task GetByKeyAsync(string key)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = from g in dbContext.Grants
+ where g.Key == key
+ select g;
+ var grant = await query.FirstOrDefaultAsync();
+ return grant;
+ }
+ }
+
+ public async Task> GetManyAsync(string subjectId, string sessionId, string clientId, string type)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = from g in dbContext.Grants
+ where g.SubjectId == subjectId &&
+ g.ClientId == clientId &&
+ g.SessionId == sessionId &&
+ g.Type == type
+ select g;
+ var grants = await query.ToListAsync();
+ return (ICollection)grants;
+ }
+ }
+
+ public async Task SaveAsync(Grant obj)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var existingGrant = await (from g in dbContext.Grants
+ where g.Key == obj.Key
+ select g).FirstOrDefaultAsync();
+ if (existingGrant != null)
+ {
+ dbContext.Entry(existingGrant).CurrentValues.SetValues(obj);
+ }
+ else
+ {
+ var entity = Mapper.Map(obj);
+ await dbContext.AddAsync(entity);
+ await dbContext.SaveChangesAsync();
+ }
+ }
+ }
+ }
+}
+
diff --git a/src/Core/Repositories/EntityFramework/GroupRepository.cs b/src/Core/Repositories/EntityFramework/GroupRepository.cs
new file mode 100644
index 0000000000..863e3ae6e8
--- /dev/null
+++ b/src/Core/Repositories/EntityFramework/GroupRepository.cs
@@ -0,0 +1,174 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using AutoMapper;
+using Bit.Core.Models.Data;
+using Bit.Core.Models.Table;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using DataModel = Bit.Core.Models.Data;
+using EfModel = Bit.Core.Models.EntityFramework;
+using TableModel = Bit.Core.Models.Table;
+
+namespace Bit.Core.Repositories.EntityFramework
+{
+ public class GroupRepository : Repository, IGroupRepository
+ {
+ public GroupRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
+ : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.Groups)
+ { }
+
+ public async Task CreateAsync(Group obj, IEnumerable collections)
+ {
+ var grp = await base.CreateAsync(obj);
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var availibleCollections = await (
+ from c in dbContext.Collections
+ where c.OrganizationId == grp.OrganizationId
+ select c).ToListAsync();
+ var filteredCollections = collections.Where(c => availibleCollections.Any(a => c.Id == a.Id));
+ var collectionGroups = filteredCollections.Select(y => new EfModel.CollectionGroup
+ {
+ CollectionId = y.Id,
+ GroupId = grp.Id,
+ ReadOnly = y.ReadOnly,
+ HidePasswords = y.HidePasswords,
+ });
+ await dbContext.CollectionGroups.AddRangeAsync(collectionGroups);
+ await dbContext.SaveChangesAsync();
+ }
+ }
+
+ public async Task DeleteUserAsync(Guid groupId, Guid organizationUserId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = from gu in dbContext.GroupUsers
+ where gu.GroupId == groupId &&
+ gu.OrganizationUserId == organizationUserId
+ select gu;
+ dbContext.RemoveRange(await query.ToListAsync());
+ await dbContext.SaveChangesAsync();
+ }
+ }
+
+ public async Task>> GetByIdWithCollectionsAsync(Guid id)
+ {
+ var grp = await base.GetByIdAsync(id);
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query = await (
+ from cg in dbContext.CollectionGroups
+ where cg.GroupId == id
+ select cg).ToListAsync();
+ var collections = query.Select(c => new SelectionReadOnly
+ {
+ Id = c.CollectionId,
+ ReadOnly = c.ReadOnly,
+ HidePasswords = c.HidePasswords,
+ }).ToList();
+ return new Tuple>(
+ grp, collections);
+ }
+ }
+
+ public async Task> GetManyByOrganizationIdAsync(Guid organizationId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var data = await (
+ from g in dbContext.Groups
+ where g.OrganizationId == organizationId
+ select g).ToListAsync();
+ return Mapper.Map>(data);
+ }
+ }
+
+ public async Task> GetManyGroupUsersByOrganizationIdAsync(Guid organizationId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query =
+ from gu in dbContext.GroupUsers
+ join g in dbContext.Groups
+ on gu.GroupId equals g.Id
+ where g.OrganizationId == organizationId
+ select gu;
+ var groupUsers = await query.ToListAsync();
+ return Mapper.Map>(groupUsers);
+ }
+ }
+
+ public async Task> GetManyIdsByUserIdAsync(Guid organizationUserId)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query =
+ from gu in dbContext.GroupUsers
+ where gu.OrganizationUserId == organizationUserId
+ select gu;
+ var groupIds = await query.Select(x => x.GroupId).ToListAsync();
+ return groupIds;
+ }
+ }
+
+ public async Task> GetManyUserIdsByIdAsync(Guid id)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ var query =
+ from gu in dbContext.GroupUsers
+ where gu.GroupId == id
+ select gu;
+ var groupIds = await query.Select(x => x.OrganizationUserId).ToListAsync();
+ return groupIds;
+ }
+ }
+
+ public async Task ReplaceAsync(Group obj, IEnumerable collections)
+ {
+ await base.ReplaceAsync(obj);
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+ await UserBumpAccountRevisionDateByOrganizationId(obj.OrganizationId);
+ }
+ }
+
+ public async Task UpdateUsersAsync(Guid groupId, IEnumerable