1
0
mirror of https://github.com/bitwarden/server synced 2026-01-02 16:43:25 +00:00

Merge branch 'master' into feature/billing-obfuscation

This commit is contained in:
Rui Tome
2023-02-27 14:52:51 +00:00
323 changed files with 39115 additions and 2469 deletions

View File

@@ -0,0 +1,31 @@
using Bit.Core;
using Bit.Core.Jobs;
using Bit.Core.Services;
using Quartz;
namespace Bit.Admin.Jobs;
public class DeleteUnverifiedOrganizationDomainsJob : BaseJob
{
private readonly IServiceProvider _serviceProvider;
public DeleteUnverifiedOrganizationDomainsJob(
IServiceProvider serviceProvider,
ILogger<DeleteUnverifiedOrganizationDomainsJob> logger)
: base(logger)
{
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteJobAsync(IJobExecutionContext context)
{
_logger.LogInformation(Constants.BypassFiltersEventId, "Execute job task: DeleteUnverifiedOrganizationDomainsJob: Start");
using (var serviceScope = _serviceProvider.CreateScope())
{
var organizationDomainService =
serviceScope.ServiceProvider.GetRequiredService<IOrganizationDomainService>();
await organizationDomainService.OrganizationDomainMaintenanceAsync();
}
_logger.LogInformation(Constants.BypassFiltersEventId, "Execute job task: DeleteUnverifiedOrganizationDomainsJob: End");
}
}

View File

@@ -64,6 +64,11 @@ public class JobsHostedService : BaseJobsHostedService
.StartNow()
.WithCronSchedule("0 */15 * ? * *")
.Build();
var everyDayAtTwoAmUtcTrigger = TriggerBuilder.Create()
.WithIdentity("EveryDayAtTwoAmUtcTrigger")
.StartNow()
.WithCronSchedule("0 0 2 ? * * *")
.Build();
var jobs = new List<Tuple<Type, ITrigger>>
{
@@ -74,6 +79,7 @@ public class JobsHostedService : BaseJobsHostedService
new Tuple<Type, ITrigger>(typeof(DeleteCiphersJob), everyDayAtMidnightUtc),
new Tuple<Type, ITrigger>(typeof(DatabaseExpiredSponsorshipsJob), everyMondayAtMidnightTrigger),
new Tuple<Type, ITrigger>(typeof(DeleteAuthRequestsJob), everyFifteenMinutesTrigger),
new Tuple<Type, ITrigger>(typeof(DeleteUnverifiedOrganizationDomainsJob), everyDayAtTwoAmUtcTrigger),
};
if (!_globalSettings.SelfHosted)
@@ -98,5 +104,6 @@ public class JobsHostedService : BaseJobsHostedService
services.AddTransient<DeleteSendsJob>();
services.AddTransient<DeleteCiphersJob>();
services.AddTransient<DeleteAuthRequestsJob>();
services.AddTransient<DeleteUnverifiedOrganizationDomainsJob>();
}
}

View File

@@ -20,7 +20,7 @@ public class OrganizationViewModel
UserInvitedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Invited);
UserAcceptedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Accepted);
UserConfirmedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Confirmed);
OccupiedSeatCount = orgUsers.Count(u => u.OccupiesOrganizationSeat);
OccupiedSeatCount = UserInvitedCount + UserAcceptedCount + UserConfirmedCount;
CipherCount = ciphers.Count();
CollectionCount = collections.Count();
GroupCount = groups?.Count() ?? 0;

View File

@@ -94,8 +94,8 @@
},
"Azure.Core": {
"type": "Transitive",
"resolved": "1.24.0",
"contentHash": "+/qI1j2oU1S4/nvxb2k/wDsol00iGf1AyJX5g3epV7eOpQEP/2xcgh/cxgKMeFgn3U2fmgSiBnQZdkV+l5y0Uw==",
"resolved": "1.25.0",
"contentHash": "X8Dd4sAggS84KScWIjEbFAdt2U1KDolQopTPoHVubG2y3CM54f9l6asVrP5Uy384NWXjsspPYaJgz5xHc+KvTA==",
"dependencies": {
"Microsoft.Bcl.AsyncInterfaces": "1.1.1",
"System.Diagnostics.DiagnosticSource": "4.6.0",
@@ -132,28 +132,28 @@
},
"Azure.Storage.Blobs": {
"type": "Transitive",
"resolved": "12.11.0",
"contentHash": "50eRjIhY7Q1JN7kT2MSawDKCcwSb7uRZUkz00P/BLjSg47gm2hxUYsnJPyvzCHntYMbOWzrvaVQTwYwXabaR5Q==",
"resolved": "12.14.1",
"contentHash": "DvRBWUDMB2LjdRbsBNtz/LiVIYk56hqzSooxx4uq4rCdLj2M+7Vvoa1r+W35Dz6ZXL6p+SNcgEae3oZ+CkPfow==",
"dependencies": {
"Azure.Storage.Common": "12.10.0",
"Azure.Storage.Common": "12.13.0",
"System.Text.Json": "4.7.2"
}
},
"Azure.Storage.Common": {
"type": "Transitive",
"resolved": "12.10.0",
"contentHash": "vYkHGzUkdZTace/cDPZLG+Mh/EoPqQuGxDIBOau9D+XWoDPmuUFGk325aXplkFE4JFGpSwoytNYzk/qBCaiHqg==",
"resolved": "12.13.0",
"contentHash": "jDv8xJWeZY2Er9zA6QO25BiGolxg87rItt9CwAp7L/V9EPJeaz8oJydaNL9Wj0+3ncceoMgdiyEv66OF8YUwWQ==",
"dependencies": {
"Azure.Core": "1.22.0",
"Azure.Core": "1.25.0",
"System.IO.Hashing": "6.0.0"
}
},
"Azure.Storage.Queues": {
"type": "Transitive",
"resolved": "12.9.0",
"contentHash": "jDiyHtsCUCrWNvZW7SjJnJb46UhpdgQrWCbL8aWpapDHlq9LvbvxYpfLh4dfKAz09QiTznLMIU3i+md9+7GzqQ==",
"resolved": "12.12.0",
"contentHash": "PwrfymLYFmmOt6A0vMiDVhBV7RoOAKftzzvrbSM3W9cJKpkxAg57AhM7/wbNb3P8Uq0B73lBurkFiFzWK9PXHg==",
"dependencies": {
"Azure.Storage.Common": "12.10.0",
"Azure.Storage.Common": "12.13.0",
"System.Memory.Data": "1.0.2",
"System.Text.Json": "4.7.2"
}
@@ -182,21 +182,29 @@
},
"dbup-core": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "CR00QMAtHjfeMhwxFC5haoA0q4KZ5s6Y/AdZaT6oFjySik2eFEqVasuLgWSPKSiR7ti3z01BtiR7aD3nVckAsg==",
"resolved": "5.0.8",
"contentHash": "d+3RxJDftcarp1Y7jI78HRdRWRC7VFjM+rB2CFHWDmao6OixuLrqiyEo1DeuMNrWLTR5mmE8p1YTpFOvozI9ZQ==",
"dependencies": {
"Microsoft.CSharp": "4.4.0",
"Microsoft.CSharp": "4.7.0",
"System.Diagnostics.TraceSource": "4.3.0"
}
},
"dbup-sqlserver": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "/4hy4qmbWmtbLJGq8XCH3mtlgMld2G8rbXcjNDhqkq5y6dGZDW03OI4UsnQRxBiTQD5aYOcLuycK1dCJYhkdSw==",
"resolved": "5.0.8",
"contentHash": "b954l5Zgj9qgHtm16SLq2qGLJ0gIZtrWdh6JHoUsCLMHYW+0K2Oevabquw447At4U6X2t4CNuy7ZLHYf/Z/8yg==",
"dependencies": {
"Microsoft.Azure.Services.AppAuthentication": "1.3.1",
"System.Data.SqlClient": "4.6.0",
"dbup-core": "4.5.0"
"Microsoft.Azure.Services.AppAuthentication": "1.6.2",
"Microsoft.Data.SqlClient": "5.0.1",
"dbup-core": "5.0.8"
}
},
"DnsClient": {
"type": "Transitive",
"resolved": "1.7.0",
"contentHash": "2hrXR83b5g6/ZMJOA36hXp4t56yb7G1mF3Hg6IkrHxvtyaoXRn2WVdgDPN3V8+GugOlUBbTWXgPaka4dXw1QIg==",
"dependencies": {
"Microsoft.Win32.Registry": "5.0.0"
}
},
"Fido2": {
@@ -494,10 +502,10 @@
},
"Microsoft.Azure.Services.AppAuthentication": {
"type": "Transitive",
"resolved": "1.3.1",
"contentHash": "59CEcmUSlg5nYOzcyhdoUu+EQH4wrjCKj7dNuuPMeIjDCikAON9/KQXTQLfzfWTjDwqHIRptAAj0DTBp25lFcg==",
"resolved": "1.6.2",
"contentHash": "rSQhTv43ionr9rWvE4vxIe/i73XR5hoBYfh7UUgdaVOGW1MZeikR9RmgaJhonTylimCcCuJvrU0zXsSIFOsTGw==",
"dependencies": {
"Microsoft.IdentityModel.Clients.ActiveDirectory": "4.3.0",
"Microsoft.IdentityModel.Clients.ActiveDirectory": "5.2.9",
"System.Diagnostics.Process": "4.3.0"
}
},
@@ -1081,15 +1089,22 @@
},
"Microsoft.IdentityModel.Clients.ActiveDirectory": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "IRXnTCHxwnpnGBHVnTWd8RBJk7nsBsNZVl8j20kh234bP+oBILkt+6Iw5vQg5Q+sZmALt3Oq6X3Kx7qY71XyVw==",
"resolved": "5.2.9",
"contentHash": "WhBAG/9hWiMHIXve4ZgwXP3spRwf7kFFfejf76QA5BvumgnPp8iDkDCiJugzAcpW1YaHB526z1UVxHhVT1E5qw==",
"dependencies": {
"Microsoft.CSharp": "4.3.0",
"NETStandard.Library": "1.6.1",
"System.ComponentModel.TypeConverter": "4.3.0",
"System.Dynamic.Runtime": "4.3.0",
"System.Net.Http": "4.3.4",
"System.Private.Uri": "4.3.2",
"System.Runtime.Serialization.Formatters": "4.3.0",
"System.Runtime.Serialization.Json": "4.3.0",
"System.Runtime.Serialization.Primitives": "4.3.0",
"System.Security.Cryptography.X509Certificates": "4.3.0",
"System.Security.SecureString": "4.3.0",
"System.Xml.XDocument": "4.3.0"
"System.Xml.XDocument": "4.3.0",
"System.Xml.XmlDocument": "4.3.0"
}
},
"Microsoft.IdentityModel.JsonWebTokens": {
@@ -1513,16 +1528,6 @@
"Microsoft.NETCore.Targets": "1.1.0"
}
},
"runtime.native.System.Data.SqlClient.sni": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "AJfX7owAAkMjWQYhoml5IBfXh8UyYPjktn8pK0BFGAdKgBS7HqMz1fw5vdzfZUWfhtTPDGCjgNttt46ZyEmSjg==",
"dependencies": {
"runtime.win-arm64.runtime.native.System.Data.SqlClient.sni": "4.4.0",
"runtime.win-x64.runtime.native.System.Data.SqlClient.sni": "4.4.0",
"runtime.win-x86.runtime.native.System.Data.SqlClient.sni": "4.4.0"
}
},
"runtime.native.System.IO.Compression": {
"type": "Transitive",
"resolved": "4.3.0",
@@ -1615,21 +1620,6 @@
"resolved": "4.3.2",
"contentHash": "leXiwfiIkW7Gmn7cgnNcdtNAU70SjmKW3jxGj1iKHOvdn0zRWsgv/l2OJUO5zdGdiv2VRFnAsxxhDgMzofPdWg=="
},
"runtime.win-arm64.runtime.native.System.Data.SqlClient.sni": {
"type": "Transitive",
"resolved": "4.4.0",
"contentHash": "LbrynESTp3bm5O/+jGL8v0Qg5SJlTV08lpIpFesXjF6uGNMWqFnUQbYBJwZTeua6E/Y7FIM1C54Ey1btLWupdg=="
},
"runtime.win-x64.runtime.native.System.Data.SqlClient.sni": {
"type": "Transitive",
"resolved": "4.4.0",
"contentHash": "38ugOfkYJqJoX9g6EYRlZB5U2ZJH51UP8ptxZgdpS07FgOEToV+lS11ouNK2PM12Pr6X/PpT5jK82G3DwH/SxQ=="
},
"runtime.win-x86.runtime.native.System.Data.SqlClient.sni": {
"type": "Transitive",
"resolved": "4.4.0",
"contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA=="
},
"SendGrid": {
"type": "Transitive",
"resolved": "9.27.0",
@@ -1904,29 +1894,69 @@
},
"System.Collections.NonGeneric": {
"type": "Transitive",
"resolved": "4.0.1",
"contentHash": "hMxFT2RhhlffyCdKLDXjx8WEC5JfCvNozAZxCablAuFRH74SCV4AgzE8yJCh/73bFnEoZgJ9MJmkjQ0dJmnKqA==",
"resolved": "4.3.0",
"contentHash": "prtjIEMhGUnQq6RnPEYLpFt8AtLbp9yq2zxOSrY7KJJZrw25Fi97IzBqY7iqssbM61Ek5b8f3MG/sG1N2sN5KA==",
"dependencies": {
"System.Diagnostics.Debug": "4.0.11",
"System.Globalization": "4.0.11",
"System.Resources.ResourceManager": "4.0.1",
"System.Runtime": "4.1.0",
"System.Runtime.Extensions": "4.1.0",
"System.Threading": "4.0.11"
"System.Diagnostics.Debug": "4.3.0",
"System.Globalization": "4.3.0",
"System.Resources.ResourceManager": "4.3.0",
"System.Runtime": "4.3.0",
"System.Runtime.Extensions": "4.3.0",
"System.Threading": "4.3.0"
}
},
"System.Collections.Specialized": {
"type": "Transitive",
"resolved": "4.0.1",
"contentHash": "/HKQyVP0yH1I0YtK7KJL/28snxHNH/bi+0lgk/+MbURF6ULhAE31MDI+NZDerNWu264YbxklXCCygISgm+HMug==",
"resolved": "4.3.0",
"contentHash": "Epx8PoVZR0iuOnJJDzp7pWvdfMMOAvpUo95pC4ScH2mJuXkKA2Y4aR3cG9qt2klHgSons1WFh4kcGW7cSXvrxg==",
"dependencies": {
"System.Collections.NonGeneric": "4.0.1",
"System.Globalization": "4.0.11",
"System.Globalization.Extensions": "4.0.1",
"System.Resources.ResourceManager": "4.0.1",
"System.Runtime": "4.1.0",
"System.Runtime.Extensions": "4.1.0",
"System.Threading": "4.0.11"
"System.Collections.NonGeneric": "4.3.0",
"System.Globalization": "4.3.0",
"System.Globalization.Extensions": "4.3.0",
"System.Resources.ResourceManager": "4.3.0",
"System.Runtime": "4.3.0",
"System.Runtime.Extensions": "4.3.0",
"System.Threading": "4.3.0"
}
},
"System.ComponentModel": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "VyGn1jGRZVfxnh8EdvDCi71v3bMXrsu8aYJOwoV7SNDLVhiEqwP86pPMyRGsDsxhXAm2b3o9OIqeETfN5qfezw==",
"dependencies": {
"System.Runtime": "4.3.0"
}
},
"System.ComponentModel.Primitives": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "j8GUkCpM8V4d4vhLIIoBLGey2Z5bCkMVNjEZseyAlm4n5arcsJOeI3zkUP+zvZgzsbLTYh4lYeP/ZD/gdIAPrw==",
"dependencies": {
"System.ComponentModel": "4.3.0",
"System.Resources.ResourceManager": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.ComponentModel.TypeConverter": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "16pQ6P+EdhcXzPiEK4kbA953Fu0MNG2ovxTZU81/qsCd1zPRsKc3uif5NgvllCY598k6bI0KUyKW8fanlfaDQg==",
"dependencies": {
"System.Collections": "4.3.0",
"System.Collections.NonGeneric": "4.3.0",
"System.Collections.Specialized": "4.3.0",
"System.ComponentModel": "4.3.0",
"System.ComponentModel.Primitives": "4.3.0",
"System.Globalization": "4.3.0",
"System.Linq": "4.3.0",
"System.Reflection": "4.3.0",
"System.Reflection.Extensions": "4.3.0",
"System.Reflection.Primitives": "4.3.0",
"System.Reflection.TypeExtensions": "4.3.0",
"System.Resources.ResourceManager": "4.3.0",
"System.Runtime": "4.3.0",
"System.Runtime.Extensions": "4.3.0",
"System.Threading": "4.3.0"
}
},
"System.Composition": {
@@ -2046,17 +2076,6 @@
"System.Text.Encoding": "4.3.0"
}
},
"System.Data.SqlClient": {
"type": "Transitive",
"resolved": "4.6.0",
"contentHash": "gwItUWW1BMCckicFO85c8frFaMK8SGqYn5IeA3GSX4Lmid+CjXETfoHz7Uv+Vx6L0No7iRc/7cBL8gd6o9k9/g==",
"dependencies": {
"Microsoft.Win32.Registry": "4.5.0",
"System.Security.Principal.Windows": "4.5.0",
"System.Text.Encoding.CodePages": "4.5.0",
"runtime.native.System.Data.SqlClient.sni": "4.5.0"
}
},
"System.Diagnostics.Debug": {
"type": "Transitive",
"resolved": "4.3.0",
@@ -2160,24 +2179,23 @@
},
"System.Dynamic.Runtime": {
"type": "Transitive",
"resolved": "4.0.11",
"contentHash": "db34f6LHYM0U0JpE+sOmjar27BnqTVkbLJhgfwMpTdgTigG/Hna3m2MYVwnFzGGKnEJk2UXFuoVTr8WUbU91/A==",
"resolved": "4.3.0",
"contentHash": "SNVi1E/vfWUAs/WYKhE9+qlS6KqK0YVhnlT0HQtr8pMIA8YX3lwy3uPMownDwdYISBdmAF/2holEIldVp85Wag==",
"dependencies": {
"System.Collections": "4.0.11",
"System.Diagnostics.Debug": "4.0.11",
"System.Globalization": "4.0.11",
"System.Linq": "4.1.0",
"System.Linq.Expressions": "4.1.0",
"System.ObjectModel": "4.0.12",
"System.Reflection": "4.1.0",
"System.Reflection.Emit": "4.0.1",
"System.Reflection.Emit.ILGeneration": "4.0.1",
"System.Reflection.Primitives": "4.0.1",
"System.Reflection.TypeExtensions": "4.1.0",
"System.Resources.ResourceManager": "4.0.1",
"System.Runtime": "4.1.0",
"System.Runtime.Extensions": "4.1.0",
"System.Threading": "4.0.11"
"System.Collections": "4.3.0",
"System.Diagnostics.Debug": "4.3.0",
"System.Linq": "4.3.0",
"System.Linq.Expressions": "4.3.0",
"System.ObjectModel": "4.3.0",
"System.Reflection": "4.3.0",
"System.Reflection.Emit": "4.3.0",
"System.Reflection.Emit.ILGeneration": "4.3.0",
"System.Reflection.Primitives": "4.3.0",
"System.Reflection.TypeExtensions": "4.3.0",
"System.Resources.ResourceManager": "4.3.0",
"System.Runtime": "4.3.0",
"System.Runtime.Extensions": "4.3.0",
"System.Threading": "4.3.0"
}
},
"System.Formats.Asn1": {
@@ -2808,6 +2826,18 @@
"System.Runtime.Extensions": "4.3.0"
}
},
"System.Runtime.Serialization.Formatters": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "KT591AkTNFOTbhZlaeMVvfax3RqhH1EJlcwF50Wm7sfnBLuHiOeZRRKrr1ns3NESkM20KPZ5Ol/ueMq5vg4QoQ==",
"dependencies": {
"System.Collections": "4.3.0",
"System.Reflection": "4.3.0",
"System.Resources.ResourceManager": "4.3.0",
"System.Runtime": "4.3.0",
"System.Runtime.Serialization.Primitives": "4.3.0"
}
},
"System.Runtime.Serialization.Json": {
"type": "Transitive",
"resolved": "4.3.0",
@@ -3299,7 +3329,7 @@
"commercial.core": {
"type": "Project",
"dependencies": {
"Core": "[2023.1.0, )"
"Core": "[2023.2.0, )"
}
},
"core": {
@@ -3310,10 +3340,11 @@
"AspNetCoreRateLimit": "[4.0.2, )",
"AspNetCoreRateLimit.Redis": "[1.0.1, )",
"Azure.Extensions.AspNetCore.DataProtection.Blobs": "[1.2.1, )",
"Azure.Storage.Blobs": "[12.11.0, )",
"Azure.Storage.Queues": "[12.9.0, )",
"Azure.Storage.Blobs": "[12.14.1, )",
"Azure.Storage.Queues": "[12.12.0, )",
"BitPay.Light": "[1.0.1907, )",
"Braintree": "[5.12.0, )",
"DnsClient": "[1.7.0, )",
"Fido2.AspNet": "[3.0.1, )",
"Handlebars.Net": "[2.1.2, )",
"IdentityServer4": "[4.1.2, )",
@@ -3345,7 +3376,7 @@
"infrastructure.dapper": {
"type": "Project",
"dependencies": {
"Core": "[2023.1.0, )",
"Core": "[2023.2.0, )",
"Dapper": "[2.0.123, )"
}
},
@@ -3353,7 +3384,7 @@
"type": "Project",
"dependencies": {
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[11.0.0, )",
"Core": "[2023.1.0, )",
"Core": "[2023.2.0, )",
"Microsoft.EntityFrameworkCore.Relational": "[6.0.12, )",
"Microsoft.EntityFrameworkCore.SqlServer": "[6.0.12, )",
"Microsoft.EntityFrameworkCore.Sqlite": "[6.0.12, )",
@@ -3365,38 +3396,38 @@
"migrator": {
"type": "Project",
"dependencies": {
"Core": "[2023.1.0, )",
"Core": "[2023.2.0, )",
"Microsoft.Extensions.Logging": "[6.0.0, )",
"dbup-sqlserver": "[4.5.0, )"
"dbup-sqlserver": "[5.0.8, )"
}
},
"mysqlmigrations": {
"type": "Project",
"dependencies": {
"Core": "[2023.1.0, )",
"Infrastructure.EntityFramework": "[2023.1.0, )"
"Core": "[2023.2.0, )",
"Infrastructure.EntityFramework": "[2023.2.0, )"
}
},
"postgresmigrations": {
"type": "Project",
"dependencies": {
"Core": "[2023.1.0, )",
"Infrastructure.EntityFramework": "[2023.1.0, )"
"Core": "[2023.2.0, )",
"Infrastructure.EntityFramework": "[2023.2.0, )"
}
},
"sharedweb": {
"type": "Project",
"dependencies": {
"Core": "[2023.1.0, )",
"Infrastructure.Dapper": "[2023.1.0, )",
"Infrastructure.EntityFramework": "[2023.1.0, )"
"Core": "[2023.2.0, )",
"Infrastructure.Dapper": "[2023.2.0, )",
"Infrastructure.EntityFramework": "[2023.2.0, )"
}
},
"sqlitemigrations": {
"type": "Project",
"dependencies": {
"Core": "[2023.1.0, )",
"Infrastructure.EntityFramework": "[2023.1.0, )"
"Core": "[2023.2.0, )",
"Infrastructure.EntityFramework": "[2023.2.0, )"
}
}
}

View File

@@ -0,0 +1,143 @@
using Bit.Api.Models.Request;
using Bit.Api.Models.Request.Organizations;
using Bit.Api.Models.Response;
using Bit.Api.Models.Response.Organizations;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.Repositories;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Controllers;
[Route("organizations")]
[Authorize("Application")]
public class OrganizationDomainController : Controller
{
private readonly ICreateOrganizationDomainCommand _createOrganizationDomainCommand;
private readonly IVerifyOrganizationDomainCommand _verifyOrganizationDomainCommand;
private readonly IDeleteOrganizationDomainCommand _deleteOrganizationDomainCommand;
private readonly IGetOrganizationDomainByIdQuery _getOrganizationDomainByIdQuery;
private readonly IGetOrganizationDomainByOrganizationIdQuery _getOrganizationDomainByOrganizationIdQuery;
private readonly ICurrentContext _currentContext;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationDomainRepository _organizationDomainRepository;
public OrganizationDomainController(
ICreateOrganizationDomainCommand createOrganizationDomainCommand,
IVerifyOrganizationDomainCommand verifyOrganizationDomainCommand,
IDeleteOrganizationDomainCommand deleteOrganizationDomainCommand,
IGetOrganizationDomainByIdQuery getOrganizationDomainByIdQuery,
IGetOrganizationDomainByOrganizationIdQuery getOrganizationDomainByOrganizationIdQuery,
ICurrentContext currentContext,
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository)
{
_createOrganizationDomainCommand = createOrganizationDomainCommand;
_verifyOrganizationDomainCommand = verifyOrganizationDomainCommand;
_deleteOrganizationDomainCommand = deleteOrganizationDomainCommand;
_getOrganizationDomainByIdQuery = getOrganizationDomainByIdQuery;
_getOrganizationDomainByOrganizationIdQuery = getOrganizationDomainByOrganizationIdQuery;
_currentContext = currentContext;
_organizationRepository = organizationRepository;
_organizationDomainRepository = organizationDomainRepository;
}
[HttpGet("{orgId}/domain")]
public async Task<ListResponseModel<OrganizationDomainResponseModel>> Get(string orgId)
{
var orgIdGuid = new Guid(orgId);
await ValidateOrganizationAccessAsync(orgIdGuid);
var domains = await _getOrganizationDomainByOrganizationIdQuery
.GetDomainsByOrganizationId(orgIdGuid);
var response = domains.Select(x => new OrganizationDomainResponseModel(x)).ToList();
return new ListResponseModel<OrganizationDomainResponseModel>(response);
}
[HttpGet("{orgId}/domain/{id}")]
public async Task<OrganizationDomainResponseModel> Get(string orgId, string id)
{
var orgIdGuid = new Guid(orgId);
var IdGuid = new Guid(id);
await ValidateOrganizationAccessAsync(orgIdGuid);
var domain = await _getOrganizationDomainByIdQuery.GetOrganizationDomainById(IdGuid);
if (domain is null)
{
throw new NotFoundException();
}
return new OrganizationDomainResponseModel(domain);
}
[HttpPost("{orgId}/domain")]
public async Task<OrganizationDomainResponseModel> Post(string orgId,
[FromBody] OrganizationDomainRequestModel model)
{
var orgIdGuid = new Guid(orgId);
await ValidateOrganizationAccessAsync(orgIdGuid);
var organizationDomain = new OrganizationDomain
{
OrganizationId = orgIdGuid,
Txt = model.Txt,
DomainName = model.DomainName.ToLower()
};
var domain = await _createOrganizationDomainCommand.CreateAsync(organizationDomain);
return new OrganizationDomainResponseModel(domain);
}
[HttpPost("{orgId}/domain/{id}/verify")]
public async Task<OrganizationDomainResponseModel> Verify(string orgId, string id)
{
var orgIdGuid = new Guid(orgId);
var idGuid = new Guid(id);
await ValidateOrganizationAccessAsync(orgIdGuid);
var domain = await _verifyOrganizationDomainCommand.VerifyOrganizationDomain(idGuid);
return new OrganizationDomainResponseModel(domain);
}
[HttpDelete("{orgId}/domain/{id}")]
[HttpPost("{orgId}/domain/{id}/remove")]
public async Task RemoveDomain(string orgId, string id)
{
var orgIdGuid = new Guid(orgId);
var idGuid = new Guid(id);
await ValidateOrganizationAccessAsync(orgIdGuid);
await _deleteOrganizationDomainCommand.DeleteAsync(idGuid);
}
[AllowAnonymous]
[HttpPost("domain/sso/details")] // must be post to accept email cleanly
public async Task<OrganizationDomainSsoDetailsResponseModel> GetOrgDomainSsoDetails(
[FromBody] OrganizationDomainSsoDetailsRequestModel model)
{
var ssoResult = await _organizationDomainRepository.GetOrganizationDomainSsoDetailsAsync(model.Email);
if (ssoResult is null)
{
throw new NotFoundException("Claimed org domain not found");
}
return new OrganizationDomainSsoDetailsResponseModel(ssoResult);
}
private async Task ValidateOrganizationAccessAsync(Guid orgIdGuid)
{
if (!await _currentContext.ManageSso(orgIdGuid))
{
throw new UnauthorizedAccessException();
}
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
if (organization == null)
{
throw new NotFoundException();
}
}
}

View File

@@ -4,6 +4,7 @@ using Bit.Api.Models.Request.Accounts;
using Bit.Api.Models.Request.Organizations;
using Bit.Api.Models.Response;
using Bit.Api.Models.Response.Organizations;
using Bit.Api.SecretsManager;
using Bit.Api.Utilities;
using Bit.Core.Context;
using Bit.Core.Enums;
@@ -38,6 +39,7 @@ public class OrganizationsController : Controller
private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand;
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
private readonly IUpdateOrganizationLicenseCommand _updateOrganizationLicenseCommand;
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
private readonly GlobalSettings _globalSettings;
@@ -55,6 +57,7 @@ public class OrganizationsController : Controller
IRotateOrganizationApiKeyCommand rotateOrganizationApiKeyCommand,
ICreateOrganizationApiKeyCommand createOrganizationApiKeyCommand,
IOrganizationApiKeyRepository organizationApiKeyRepository,
IUpdateOrganizationLicenseCommand updateOrganizationLicenseCommand,
ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery,
GlobalSettings globalSettings)
{
@@ -71,6 +74,7 @@ public class OrganizationsController : Controller
_rotateOrganizationApiKeyCommand = rotateOrganizationApiKeyCommand;
_createOrganizationApiKeyCommand = createOrganizationApiKeyCommand;
_organizationApiKeyRepository = organizationApiKeyRepository;
_updateOrganizationLicenseCommand = updateOrganizationLicenseCommand;
_cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery;
_globalSettings = globalSettings;
}
@@ -135,6 +139,7 @@ public class OrganizationsController : Controller
{
throw new NotFoundException();
}
return new OrganizationSubscriptionResponseModel(organization, subscriptionInfo);
}
else
@@ -255,7 +260,7 @@ public class OrganizationsController : Controller
}
var updateBilling = !_globalSettings.SelfHosted && (model.BusinessName != organization.BusinessName ||
model.BillingEmail != organization.BillingEmail);
model.BillingEmail != organization.BillingEmail);
var hasRequiredPermissions = updateBilling
? await _currentContext.ManageBilling(orgIdGuid)
@@ -304,11 +309,7 @@ public class OrganizationsController : Controller
}
var result = await _organizationService.UpgradePlanAsync(orgIdGuid, model.ToOrganizationUpgrade());
return new PaymentResponseModel
{
Success = result.Item1,
PaymentIntentClientSecret = result.Item2
};
return new PaymentResponseModel { Success = result.Item1, PaymentIntentClientSecret = result.Item2 };
}
[HttpPost("{id}/subscription")]
@@ -335,11 +336,7 @@ public class OrganizationsController : Controller
}
var result = await _organizationService.AdjustSeatsAsync(orgIdGuid, model.SeatAdjustment.Value);
return new PaymentResponseModel
{
Success = true,
PaymentIntentClientSecret = result
};
return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result };
}
[HttpPost("{id}/storage")]
@@ -353,11 +350,7 @@ public class OrganizationsController : Controller
}
var result = await _organizationService.AdjustStorageAsync(orgIdGuid, model.StorageGbAdjustment.Value);
return new PaymentResponseModel
{
Success = true,
PaymentIntentClientSecret = result
};
return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result };
}
[HttpPost("{id}/verify-bank")]
@@ -471,7 +464,15 @@ public class OrganizationsController : Controller
throw new BadRequestException("Invalid license");
}
await _organizationService.UpdateLicenseAsync(new Guid(id), license);
var selfHostedOrganizationDetails = await _organizationRepository.GetSelfHostedOrganizationDetailsById(orgIdGuid);
if (selfHostedOrganizationDetails == null)
{
throw new NotFoundException();
}
var existingOrganization = await _organizationRepository.GetByLicenseKeyAsync(license.LicenseKey);
await _updateOrganizationLicenseCommand.UpdateLicenseAsync(selfHostedOrganizationDetails, license, existingOrganization);
}
[HttpPost("{id}/import")]
@@ -548,7 +549,8 @@ public class OrganizationsController : Controller
}
[HttpGet("{id}/api-key-information/{type?}")]
public async Task<ListResponseModel<OrganizationApiKeyInformation>> ApiKeyInformation(Guid id, [FromRoute] OrganizationApiKeyType? type)
public async Task<ListResponseModel<OrganizationApiKeyInformation>> ApiKeyInformation(Guid id,
[FromRoute] OrganizationApiKeyType? type)
{
if (!await HasApiKeyAccessAsync(id, type))
{
@@ -577,8 +579,8 @@ public class OrganizationsController : Controller
}
var organizationApiKey = await _getOrganizationApiKeyQuery
.GetOrganizationApiKeyAsync(organization.Id, model.Type) ??
await _createOrganizationApiKeyCommand.CreateAsync(organization.Id, model.Type);
.GetOrganizationApiKeyAsync(organization.Id, model.Type) ??
await _createOrganizationApiKeyCommand.CreateAsync(organization.Id, model.Type);
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
@@ -679,7 +681,8 @@ public class OrganizationsController : Controller
throw new UnauthorizedAccessException();
}
var org = await _organizationService.UpdateOrganizationKeysAsync(new Guid(id), model.PublicKey, model.EncryptedPrivateKey);
var org = await _organizationService.UpdateOrganizationKeysAsync(new Guid(id), model.PublicKey,
model.EncryptedPrivateKey);
return new OrganizationKeysResponseModel(org);
}
@@ -725,4 +728,34 @@ public class OrganizationsController : Controller
return new OrganizationSsoResponseModel(organization, _globalSettings, ssoConfig);
}
// This is a temporary endpoint to self-enroll in secrets manager
[SecretsManager]
[SelfHosted(NotSelfHostedOnly = true)]
[HttpPost("{id}/enroll-secrets-manager")]
public async Task EnrollSecretsManager(Guid id, [FromBody] OrganizationEnrollSecretsManagerRequestModel model)
{
var userId = _userService.GetProperUserId(User).Value;
if (!await _currentContext.OrganizationAdmin(id))
{
throw new NotFoundException();
}
var organization = await _organizationRepository.GetByIdAsync(id);
if (organization == null)
{
throw new NotFoundException();
}
organization.UseSecretsManager = model.Enabled;
await _organizationService.UpdateAsync(organization);
// Turn on Secrets Manager for the user
if (model.Enabled)
{
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(id, userId);
orgUser.AccessSecretsManager = true;
await _organizationUserRepository.ReplaceAsync(orgUser);
}
}
}

View File

@@ -27,6 +27,7 @@ public class SelfHostedOrganizationLicensesController : Controller
private readonly IOrganizationService _organizationService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IUserService _userService;
private readonly IUpdateOrganizationLicenseCommand _updateOrganizationLicenseCommand;
public SelfHostedOrganizationLicensesController(
ICurrentContext currentContext,
@@ -34,7 +35,8 @@ public class SelfHostedOrganizationLicensesController : Controller
IOrganizationConnectionRepository organizationConnectionRepository,
IOrganizationService organizationService,
IOrganizationRepository organizationRepository,
IUserService userService)
IUserService userService,
IUpdateOrganizationLicenseCommand updateOrganizationLicenseCommand)
{
_currentContext = currentContext;
_selfHostedGetOrganizationLicenseQuery = selfHostedGetOrganizationLicenseQuery;
@@ -42,6 +44,7 @@ public class SelfHostedOrganizationLicensesController : Controller
_organizationService = organizationService;
_organizationRepository = organizationRepository;
_userService = userService;
_updateOrganizationLicenseCommand = updateOrganizationLicenseCommand;
}
[HttpPost("")]
@@ -79,25 +82,33 @@ public class SelfHostedOrganizationLicensesController : Controller
throw new BadRequestException("Invalid license");
}
await _organizationService.UpdateLicenseAsync(new Guid(id), license);
var selfHostedOrganizationDetails = await _organizationRepository.GetSelfHostedOrganizationDetailsById(orgIdGuid);
if (selfHostedOrganizationDetails == null)
{
throw new NotFoundException();
}
var currentOrganization = await _organizationRepository.GetByLicenseKeyAsync(license.LicenseKey);
await _updateOrganizationLicenseCommand.UpdateLicenseAsync(selfHostedOrganizationDetails, license, currentOrganization);
}
[HttpPost("{id}/sync")]
public async Task SyncLicenseAsync(string id)
{
var organization = await _organizationRepository.GetByIdAsync(new Guid(id));
if (organization == null)
var selfHostedOrganizationDetails = await _organizationRepository.GetSelfHostedOrganizationDetailsById(new Guid(id));
if (selfHostedOrganizationDetails == null)
{
throw new NotFoundException();
}
if (!await _currentContext.OrganizationOwner(organization.Id))
if (!await _currentContext.OrganizationOwner(selfHostedOrganizationDetails.Id))
{
throw new NotFoundException();
}
var billingSyncConnection =
(await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organization.Id,
(await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(selfHostedOrganizationDetails.Id,
OrganizationConnectionType.CloudBillingSync)).FirstOrDefault();
if (billingSyncConnection == null)
{
@@ -105,9 +116,10 @@ public class SelfHostedOrganizationLicensesController : Controller
}
var license =
await _selfHostedGetOrganizationLicenseQuery.GetLicenseAsync(organization, billingSyncConnection);
await _selfHostedGetOrganizationLicenseQuery.GetLicenseAsync(selfHostedOrganizationDetails, billingSyncConnection);
var currentOrganization = await _organizationRepository.GetByLicenseKeyAsync(license.LicenseKey);
await _organizationService.UpdateLicenseAsync(organization.Id, license);
await _updateOrganizationLicenseCommand.UpdateLicenseAsync(selfHostedOrganizationDetails, license, currentOrganization);
var config = billingSyncConnection.GetConfig<BillingSyncConfig>();
config.LastLicenseSync = DateTime.Now;

View File

@@ -11,7 +11,6 @@ using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.Core.Utilities.Duo;
using Fido2NetLib;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
@@ -153,7 +152,7 @@ public class TwoFactorController : Controller
try
{
var duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host);
duoApi.JSONApiCall<object>("GET", "/auth/v2/check");
await duoApi.JSONApiCall("GET", "/auth/v2/check");
}
catch (DuoException)
{
@@ -210,7 +209,7 @@ public class TwoFactorController : Controller
try
{
var duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host);
duoApi.JSONApiCall<object>("GET", "/auth/v2/check");
await duoApi.JSONApiCall("GET", "/auth/v2/check");
}
catch (DuoException)
{
@@ -295,21 +294,14 @@ public class TwoFactorController : Controller
if (await _verifyAuthRequestCommand
.VerifyAuthRequestAsync(new Guid(model.AuthRequestId), model.AuthRequestAccessCode))
{
var isBecauseNewDeviceLogin = await IsNewDeviceLoginAsync(user, model);
await _userService.SendTwoFactorEmailAsync(user, isBecauseNewDeviceLogin);
await _userService.SendTwoFactorEmailAsync(user);
return;
}
}
else
else if (await _userService.VerifySecretAsync(user, model.Secret))
{
if (await _userService.VerifySecretAsync(user, model.Secret))
{
var isBecauseNewDeviceLogin = await IsNewDeviceLoginAsync(user, model);
await _userService.SendTwoFactorEmailAsync(user, isBecauseNewDeviceLogin);
return;
}
await _userService.SendTwoFactorEmailAsync(user);
return;
}
}
@@ -390,41 +382,18 @@ public class TwoFactorController : Controller
}
}
[Obsolete("Leaving this for backwards compatibilty on clients")]
[HttpGet("get-device-verification-settings")]
public async Task<DeviceVerificationResponseModel> GetDeviceVerificationSettings()
public Task<DeviceVerificationResponseModel> GetDeviceVerificationSettings()
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
if (User.Claims.HasSsoIdP())
{
return new DeviceVerificationResponseModel(false, false);
}
var canUserEditDeviceVerificationSettings = _userService.CanEditDeviceVerificationSettings(user);
return new DeviceVerificationResponseModel(canUserEditDeviceVerificationSettings, canUserEditDeviceVerificationSettings && user.UnknownDeviceVerificationEnabled);
return Task.FromResult(new DeviceVerificationResponseModel(false, false));
}
[Obsolete("Leaving this for backwards compatibilty on clients")]
[HttpPut("device-verification-settings")]
public async Task<DeviceVerificationResponseModel> PutDeviceVerificationSettings([FromBody] DeviceVerificationRequestModel model)
public Task<DeviceVerificationResponseModel> PutDeviceVerificationSettings([FromBody] DeviceVerificationRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
if (!_userService.CanEditDeviceVerificationSettings(user)
|| User.Claims.HasSsoIdP())
{
throw new InvalidOperationException("Can't update device verification settings");
}
model.ToUser(user);
await _userService.SaveUserAsync(user);
return new DeviceVerificationResponseModel(true, user.UnknownDeviceVerificationEnabled);
return Task.FromResult(new DeviceVerificationResponseModel(false, false));
}
private async Task<User> CheckAsync(SecretVerificationRequestModel model, bool premium)
@@ -467,17 +436,4 @@ public class TwoFactorController : Controller
await Task.Delay(500);
}
}
private async Task<bool> IsNewDeviceLoginAsync(User user, TwoFactorEmailRequestModel model)
{
if (user.GetTwoFactorProvider(TwoFactorProviderType.Email) is null
&&
await _userService.Needs2FABecauseNewDeviceAsync(user, model.DeviceIdentifier, null))
{
model.ToUser(user);
return true;
}
return false;
}
}

View File

@@ -47,6 +47,12 @@ public class JobsHostedService : BaseJobsHostedService
.WithIntervalInHours(24)
.RepeatForever())
.Build();
var validateOrganizationDomainTrigger = TriggerBuilder.Create()
.WithIdentity("ValidateOrganizationDomainTrigger")
.StartNow()
.WithCronSchedule("0 0 * * * ?")
.Build();
var jobs = new List<Tuple<Type, ITrigger>>
{
@@ -54,7 +60,8 @@ public class JobsHostedService : BaseJobsHostedService
new Tuple<Type, ITrigger>(typeof(EmergencyAccessNotificationJob), emergencyAccessNotificationTrigger),
new Tuple<Type, ITrigger>(typeof(EmergencyAccessTimeoutJob), emergencyAccessTimeoutTrigger),
new Tuple<Type, ITrigger>(typeof(ValidateUsersJob), everyTopOfTheSixthHourTrigger),
new Tuple<Type, ITrigger>(typeof(ValidateOrganizationsJob), everyTwelfthHourAndThirtyMinutesTrigger)
new Tuple<Type, ITrigger>(typeof(ValidateOrganizationsJob), everyTwelfthHourAndThirtyMinutesTrigger),
new Tuple<Type, ITrigger>(typeof(ValidateOrganizationDomainJob), validateOrganizationDomainTrigger),
};
if (_globalSettings.SelfHosted && _globalSettings.EnableCloudCommunication)
@@ -78,5 +85,6 @@ public class JobsHostedService : BaseJobsHostedService
services.AddTransient<EmergencyAccessTimeoutJob>();
services.AddTransient<ValidateUsersJob>();
services.AddTransient<ValidateOrganizationsJob>();
services.AddTransient<ValidateOrganizationDomainJob>();
}
}

View File

@@ -0,0 +1,30 @@
using Bit.Core;
using Bit.Core.Jobs;
using Bit.Core.Services;
using Quartz;
namespace Bit.Api.Jobs;
public class ValidateOrganizationDomainJob : BaseJob
{
private readonly IServiceProvider _serviceProvider;
public ValidateOrganizationDomainJob(
IServiceProvider serviceProvider,
ILogger<ValidateOrganizationDomainJob> logger)
: base(logger)
{
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteJobAsync(IJobExecutionContext context)
{
_logger.LogInformation(Constants.BypassFiltersEventId, "Execute job task: ValidateOrganizationDomainJob: Start");
using (var serviceScope = _serviceProvider.CreateScope())
{
var organizationDomainService =
serviceScope.ServiceProvider.GetRequiredService<IOrganizationDomainService>();
await organizationDomainService.ValidateOrganizationsDomainAsync();
}
_logger.LogInformation(Constants.BypassFiltersEventId, "Execute job task: ValidateOrganizationDomainJob: End");
}
}

View File

@@ -1,16 +1,10 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Entities;
namespace Bit.Api.Models.Request;
public class DeviceVerificationRequestModel
{
[Obsolete("Leaving this for backwards compatibilty on clients")]
[Required]
public bool UnknownDeviceVerificationEnabled { get; set; }
public User ToUser(User user)
{
user.UnknownDeviceVerificationEnabled = UnknownDeviceVerificationEnabled;
return user;
}
}

View File

@@ -10,8 +10,6 @@ public class GroupRequestModel
public string Name { get; set; }
[Required]
public bool? AccessAll { get; set; }
[StringLength(300)]
public string ExternalId { get; set; }
public IEnumerable<SelectionReadOnlyRequestModel> Collections { get; set; }
public IEnumerable<Guid> Users { get; set; }
@@ -27,7 +25,6 @@ public class GroupRequestModel
{
existingGroup.Name = Name;
existingGroup.AccessAll = AccessAll.Value;
existingGroup.ExternalId = ExternalId;
return existingGroup;
}
}

View File

@@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Models.Request;
public class OrganizationDomainRequestModel
{
[Required]
public string Txt { get; set; }
[Required]
public string DomainName { get; set; }
}

View File

@@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Models.Request.Organizations;
public class OrganizationDomainSsoDetailsRequestModel
{
[Required]
[EmailAddress]
public string Email { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Bit.Api.Models.Request.Organizations;
public class OrganizationEnrollSecretsManagerRequestModel
{
public bool Enabled { get; set; }
}

View File

@@ -12,9 +12,6 @@ public class OrganizationUpdateRequestModel
public string Name { get; set; }
[StringLength(50)]
public string BusinessName { get; set; }
[Obsolete("2022-08-03 Moved to Org SSO request model, left for backwards compatability. Remove with EC-489.")]
[StringLength(50)]
public string Identifier { get; set; }
[EmailAddress]
[Required]
[StringLength(256)]
@@ -31,7 +28,6 @@ public class OrganizationUpdateRequestModel
existingOrganization.BusinessName = BusinessName;
existingOrganization.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();
}
existingOrganization.Identifier = Identifier;
Keys?.ToOrganization(existingOrganization);
return existingOrganization;
}

View File

@@ -3,6 +3,7 @@ using Bit.Api.Models.Request.Accounts;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.Utilities;
using Fido2NetLib;
namespace Bit.Api.Models.Request;
@@ -104,7 +105,7 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (!Core.Utilities.Duo.DuoApi.ValidHost(Host))
if (!DuoApi.ValidHost(Host))
{
yield return new ValidationResult("Host is invalid.", new string[] { nameof(Host) });
}
@@ -202,8 +203,6 @@ public class TwoFactorEmailRequestModel : SecretVerificationRequestModel
[StringLength(256)]
public string Email { get; set; }
public string DeviceIdentifier { get; set; }
public string AuthRequestId { get; set; }
public User ToUser(User extistingUser)

View File

@@ -2,6 +2,7 @@
namespace Bit.Api.Models.Response;
[Obsolete("Leaving this for backwards compatibilty on clients")]
public class DeviceVerificationResponseModel : ResponseModel
{
public DeviceVerificationResponseModel(bool isDeviceVerificationSectionEnabled, bool unknownDeviceVerificationEnabled)

View File

@@ -31,6 +31,9 @@ public class EventResponseModel : ResponseModel
IpAddress = ev.IpAddress;
InstallationId = ev.InstallationId;
SystemUser = ev.SystemUser;
DomainName = ev.DomainName;
SecretId = ev.SecretId;
ServiceAccountId = ev.ServiceAccountId;
}
public EventType Type { get; set; }
@@ -50,4 +53,7 @@ public class EventResponseModel : ResponseModel
public DeviceType? DeviceType { get; set; }
public string IpAddress { get; set; }
public EventSystemUser? SystemUser { get; set; }
public string DomainName { get; set; }
public Guid? SecretId { get; set; }
public Guid? ServiceAccountId { get; set; }
}

View File

@@ -0,0 +1,36 @@
using Bit.Core.Entities;
using Bit.Core.Models.Api;
namespace Bit.Api.Models.Response.Organizations;
public class OrganizationDomainResponseModel : ResponseModel
{
public OrganizationDomainResponseModel(OrganizationDomain organizationDomain, string obj = "organizationDomain")
: base(obj)
{
if (organizationDomain == null)
{
throw new ArgumentNullException(nameof(organizationDomain));
}
Id = organizationDomain.Id.ToString();
OrganizationId = organizationDomain.OrganizationId.ToString();
Txt = organizationDomain.Txt;
DomainName = organizationDomain.DomainName;
CreationDate = organizationDomain.CreationDate;
NextRunDate = organizationDomain.NextRunDate;
JobRunCount = organizationDomain.JobRunCount;
VerifiedDate = organizationDomain.VerifiedDate;
LastCheckedDate = organizationDomain.LastCheckedDate;
}
public string Id { get; set; }
public string OrganizationId { get; set; }
public string Txt { get; set; }
public string DomainName { get; set; }
public DateTime CreationDate { get; set; }
public DateTime NextRunDate { get; set; }
public int JobRunCount { get; set; }
public DateTime? VerifiedDate { get; set; }
public DateTime? LastCheckedDate { get; set; }
}

View File

@@ -0,0 +1,28 @@
using Bit.Core.Models.Api;
using Bit.Core.Models.Data.Organizations;
namespace Bit.Api.Models.Response.Organizations;
public class OrganizationDomainSsoDetailsResponseModel : ResponseModel
{
public OrganizationDomainSsoDetailsResponseModel(OrganizationDomainSsoDetailsData data, string obj = "organizationDomainSsoDetails")
: base(obj)
{
if (data == null)
{
throw new ArgumentNullException(nameof(data));
}
SsoAvailable = data.SsoAvailable;
DomainName = data.DomainName;
OrganizationIdentifier = data.OrganizationIdentifier;
SsoRequired = data.SsoRequired;
VerifiedDate = data.VerifiedDate;
}
public bool SsoAvailable { get; private set; }
public string DomainName { get; private set; }
public string OrganizationIdentifier { get; private set; }
public bool SsoRequired { get; private set; }
public DateTime? VerifiedDate { get; private set; }
}

View File

@@ -17,7 +17,6 @@ public class OrganizationResponseModel : ResponseModel
}
Id = organization.Id.ToString();
Identifier = organization.Identifier;
Name = organization.Name;
BusinessName = organization.BusinessName;
BusinessAddress1 = organization.BusinessAddress1;
@@ -51,7 +50,6 @@ public class OrganizationResponseModel : ResponseModel
}
public string Id { get; set; }
public string Identifier { get; set; }
public string Name { get; set; }
public string BusinessName { get; set; }
public string BusinessAddress1 { get; set; }

View File

@@ -23,6 +23,7 @@ public class OrganizationUserResponseModel : ResponseModel
Type = organizationUser.Type;
Status = organizationUser.Status;
AccessAll = organizationUser.AccessAll;
ExternalId = organizationUser.ExternalId;
AccessSecretsManager = organizationUser.AccessSecretsManager;
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(organizationUser.Permissions);
ResetPasswordEnrolled = !string.IsNullOrEmpty(organizationUser.ResetPasswordKey);
@@ -41,6 +42,7 @@ public class OrganizationUserResponseModel : ResponseModel
Type = organizationUser.Type;
Status = organizationUser.Status;
AccessAll = organizationUser.AccessAll;
ExternalId = organizationUser.ExternalId;
AccessSecretsManager = organizationUser.AccessSecretsManager;
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(organizationUser.Permissions);
ResetPasswordEnrolled = !string.IsNullOrEmpty(organizationUser.ResetPasswordKey);
@@ -52,6 +54,7 @@ public class OrganizationUserResponseModel : ResponseModel
public OrganizationUserType Type { get; set; }
public OrganizationUserStatusType Status { get; set; }
public bool AccessAll { get; set; }
public string ExternalId { get; set; }
public bool AccessSecretsManager { get; set; }
public Permissions Permissions { get; set; }
public bool ResetPasswordEnrolled { get; set; }
@@ -86,6 +89,7 @@ public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponse
Name = organizationUser.Name;
Email = organizationUser.Email;
AvatarColor = organizationUser.AvatarColor;
TwoFactorEnabled = twoFactorEnabled;
SsoBound = !string.IsNullOrWhiteSpace(organizationUser.SsoExternalId);
Collections = organizationUser.Collections.Select(c => new SelectionReadOnlyResponseModel(c));
@@ -94,8 +98,10 @@ public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponse
ResetPasswordEnrolled = ResetPasswordEnrolled && !organizationUser.UsesKeyConnector;
}
public string Name { get; set; }
public string Email { get; set; }
public string AvatarColor { get; set; }
public bool TwoFactorEnabled { get; set; }
public bool SsoBound { get; set; }
public IEnumerable<SelectionReadOnlyResponseModel> Collections { get; set; }

View File

@@ -1,27 +1,54 @@
using Bit.Api.SecretsManager.Models.Request;
using Bit.Api.Models.Response;
using Bit.Api.SecretsManager.Models.Request;
using Bit.Api.SecretsManager.Models.Response;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.SecretsManager.Controllers;
[SecretsManager]
[Authorize("secrets")]
[Route("access-policies")]
public class AccessPoliciesController : Controller
{
private const int _maxBulkCreation = 15;
private readonly IAccessPolicyRepository _accessPolicyRepository;
private readonly ICreateAccessPoliciesCommand _createAccessPoliciesCommand;
private readonly ICurrentContext _currentContext;
private readonly IDeleteAccessPolicyCommand _deleteAccessPolicyCommand;
private readonly IGroupRepository _groupRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IProjectRepository _projectRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IUpdateAccessPolicyCommand _updateAccessPolicyCommand;
private readonly IUserService _userService;
public AccessPoliciesController(
IUserService userService,
ICurrentContext currentContext,
IAccessPolicyRepository accessPolicyRepository,
IServiceAccountRepository serviceAccountRepository,
IGroupRepository groupRepository,
IProjectRepository projectRepository,
IOrganizationUserRepository organizationUserRepository,
ICreateAccessPoliciesCommand createAccessPoliciesCommand,
IDeleteAccessPolicyCommand deleteAccessPolicyCommand,
IUpdateAccessPolicyCommand updateAccessPolicyCommand)
{
_userService = userService;
_currentContext = currentContext;
_serviceAccountRepository = serviceAccountRepository;
_projectRepository = projectRepository;
_groupRepository = groupRepository;
_organizationUserRepository = organizationUserRepository;
_accessPolicyRepository = accessPolicyRepository;
_createAccessPoliciesCommand = createAccessPoliciesCommand;
_deleteAccessPolicyCommand = deleteAccessPolicyCommand;
@@ -32,37 +59,244 @@ public class AccessPoliciesController : Controller
public async Task<ProjectAccessPoliciesResponseModel> CreateProjectAccessPoliciesAsync([FromRoute] Guid id,
[FromBody] AccessPoliciesCreateRequest request)
{
if (request.Count() > _maxBulkCreation)
{
throw new BadRequestException($"Can process no more than {_maxBulkCreation} creation requests at once.");
}
var project = await _projectRepository.GetByIdAsync(id);
if (project == null)
{
throw new NotFoundException();
}
var (accessClient, userId) = await GetAccessClientTypeAsync(project.OrganizationId);
var policies = request.ToBaseAccessPoliciesForProject(id);
var results = await _createAccessPoliciesCommand.CreateAsync(policies);
await _createAccessPoliciesCommand.CreateManyAsync(policies, userId, accessClient);
var results = await _accessPolicyRepository.GetManyByGrantedProjectIdAsync(id);
return new ProjectAccessPoliciesResponseModel(results);
}
[HttpGet("/projects/{id}/access-policies")]
public async Task<ProjectAccessPoliciesResponseModel> GetProjectAccessPoliciesAsync([FromRoute] Guid id)
{
var project = await _projectRepository.GetByIdAsync(id);
await CheckUserHasWriteAccessToProjectAsync(project);
var results = await _accessPolicyRepository.GetManyByGrantedProjectIdAsync(id);
return new ProjectAccessPoliciesResponseModel(results);
}
[HttpPost("/service-accounts/{id}/access-policies")]
public async Task<ServiceAccountAccessPoliciesResponseModel> CreateServiceAccountAccessPoliciesAsync(
[FromRoute] Guid id,
[FromBody] AccessPoliciesCreateRequest request)
{
if (request.Count() > _maxBulkCreation)
{
throw new BadRequestException($"Can process no more than {_maxBulkCreation} creation requests at once.");
}
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
if (serviceAccount == null)
{
throw new NotFoundException();
}
var (accessClient, userId) = await GetAccessClientTypeAsync(serviceAccount.OrganizationId);
var policies = request.ToBaseAccessPoliciesForServiceAccount(id);
await _createAccessPoliciesCommand.CreateManyAsync(policies, userId, accessClient);
var results = await _accessPolicyRepository.GetManyByGrantedServiceAccountIdAsync(id);
return new ServiceAccountAccessPoliciesResponseModel(results);
}
[HttpGet("/service-accounts/{id}/access-policies")]
public async Task<ServiceAccountAccessPoliciesResponseModel> GetServiceAccountAccessPoliciesAsync(
[FromRoute] Guid id)
{
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
await CheckUserHasWriteAccessToServiceAccountAsync(serviceAccount);
var results = await _accessPolicyRepository.GetManyByGrantedServiceAccountIdAsync(id);
return new ServiceAccountAccessPoliciesResponseModel(results);
}
[HttpGet("/service-accounts/{id}/granted-policies")]
public async Task<ListResponseModel<ServiceAccountProjectAccessPolicyResponseModel>>
GetServiceAccountGrantedPoliciesAsync([FromRoute] Guid id)
{
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
if (serviceAccount == null)
{
throw new NotFoundException();
}
var (accessClient, userId) = await GetAccessClientTypeAsync(serviceAccount.OrganizationId);
var results = await _accessPolicyRepository.GetManyByServiceAccountIdAsync(id, userId, accessClient);
var responses = results.Select(ap =>
new ServiceAccountProjectAccessPolicyResponseModel((ServiceAccountProjectAccessPolicy)ap));
return new ListResponseModel<ServiceAccountProjectAccessPolicyResponseModel>(responses);
}
[HttpPost("/service-accounts/{id}/granted-policies")]
public async Task<ListResponseModel<ServiceAccountProjectAccessPolicyResponseModel>>
CreateServiceAccountGrantedPoliciesAsync([FromRoute] Guid id,
[FromBody] List<GrantedAccessPolicyRequest> requests)
{
if (requests.Count > _maxBulkCreation)
{
throw new BadRequestException($"Can process no more than {_maxBulkCreation} creation requests at once.");
}
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
if (serviceAccount == null)
{
throw new NotFoundException();
}
var (accessClient, userId) = await GetAccessClientTypeAsync(serviceAccount.OrganizationId);
var policies = requests.Select(request => request.ToServiceAccountProjectAccessPolicy(id));
var results =
await _createAccessPoliciesCommand.CreateManyAsync(new List<BaseAccessPolicy>(policies), userId, accessClient);
var responses = results.Select(ap =>
new ServiceAccountProjectAccessPolicyResponseModel((ServiceAccountProjectAccessPolicy)ap));
return new ListResponseModel<ServiceAccountProjectAccessPolicyResponseModel>(responses);
}
[HttpPut("{id}")]
public async Task<BaseAccessPolicyResponseModel> UpdateAccessPolicyAsync([FromRoute] Guid id,
[FromBody] AccessPolicyUpdateRequest request)
{
var result = await _updateAccessPolicyCommand.UpdateAsync(id, request.Read, request.Write);
var userId = _userService.GetProperUserId(User).Value;
var result = await _updateAccessPolicyCommand.UpdateAsync(id, request.Read, request.Write, userId);
return result switch
{
UserProjectAccessPolicy accessPolicy => new UserProjectAccessPolicyResponseModel(accessPolicy),
UserServiceAccountAccessPolicy accessPolicy =>
new UserServiceAccountAccessPolicyResponseModel(accessPolicy),
GroupProjectAccessPolicy accessPolicy => new GroupProjectAccessPolicyResponseModel(accessPolicy),
GroupServiceAccountAccessPolicy accessPolicy => new GroupServiceAccountAccessPolicyResponseModel(
accessPolicy),
ServiceAccountProjectAccessPolicy accessPolicy => new ServiceAccountProjectAccessPolicyResponseModel(
accessPolicy),
_ => throw new ArgumentException("Unsupported access policy type provided.")
_ => throw new ArgumentException("Unsupported access policy type provided."),
};
}
[HttpDelete("{id}")]
public async Task DeleteAccessPolicyAsync([FromRoute] Guid id)
{
await _deleteAccessPolicyCommand.DeleteAsync(id);
var userId = _userService.GetProperUserId(User).Value;
await _deleteAccessPolicyCommand.DeleteAsync(id, userId);
}
[HttpGet("/organizations/{id}/access-policies/people/potential-grantees")]
public async Task<ListResponseModel<PotentialGranteeResponseModel>> GetPeoplePotentialGranteesAsync(
[FromRoute] Guid id)
{
if (!_currentContext.AccessSecretsManager(id))
{
throw new NotFoundException();
}
var groups = await _groupRepository.GetManyByOrganizationIdAsync(id);
var groupResponses = groups.Select(g => new PotentialGranteeResponseModel(g));
var organizationUsers =
await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id);
var userResponses = organizationUsers
.Where(user => user.AccessSecretsManager)
.Select(userDetails => new PotentialGranteeResponseModel(userDetails));
return new ListResponseModel<PotentialGranteeResponseModel>(userResponses.Concat(groupResponses));
}
[HttpGet("/organizations/{id}/access-policies/service-accounts/potential-grantees")]
public async Task<ListResponseModel<PotentialGranteeResponseModel>> GetServiceAccountsPotentialGranteesAsync(
[FromRoute] Guid id)
{
var (accessClient, userId) = await GetAccessClientTypeAsync(id);
var serviceAccounts =
await _serviceAccountRepository.GetManyByOrganizationIdWriteAccessAsync(id,
userId,
accessClient);
var serviceAccountResponses =
serviceAccounts.Select(serviceAccount => new PotentialGranteeResponseModel(serviceAccount));
return new ListResponseModel<PotentialGranteeResponseModel>(serviceAccountResponses);
}
[HttpGet("/organizations/{id}/access-policies/projects/potential-grantees")]
public async Task<ListResponseModel<PotentialGranteeResponseModel>> GetProjectPotentialGranteesAsync(
[FromRoute] Guid id)
{
var (accessClient, userId) = await GetAccessClientTypeAsync(id);
var projects =
await _projectRepository.GetManyByOrganizationIdWriteAccessAsync(id,
userId,
accessClient);
var projectResponses =
projects.Select(project => new PotentialGranteeResponseModel(project));
return new ListResponseModel<PotentialGranteeResponseModel>(projectResponses);
}
private async Task CheckUserHasWriteAccessToProjectAsync(Project project)
{
if (project == null)
{
throw new NotFoundException();
}
var (accessClient, userId) = await GetAccessClientTypeAsync(project.OrganizationId);
var hasAccess = accessClient switch
{
AccessClientType.NoAccessCheck => true,
AccessClientType.User => await _projectRepository.UserHasWriteAccessToProject(project.Id, userId),
_ => false,
};
if (!hasAccess)
{
throw new NotFoundException();
}
}
private async Task CheckUserHasWriteAccessToServiceAccountAsync(ServiceAccount serviceAccount)
{
if (serviceAccount == null)
{
throw new NotFoundException();
}
var (accessClient, userId) = await GetAccessClientTypeAsync(serviceAccount.OrganizationId);
var hasAccess = accessClient switch
{
AccessClientType.NoAccessCheck => true,
AccessClientType.User => await _serviceAccountRepository.UserHasWriteAccessToServiceAccount(
serviceAccount.Id, userId),
_ => false,
};
if (!hasAccess)
{
throw new NotFoundException();
}
}
private async Task<(AccessClientType AccessClientType, Guid UserId)> GetAccessClientTypeAsync(Guid organizationId)
{
if (!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User).Value;
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
return (accessClient, userId);
}
}

View File

@@ -65,7 +65,8 @@ public class ProjectsController : Controller
throw new NotFoundException();
}
var result = await _createProjectCommand.CreateAsync(createRequest.ToProject(organizationId));
var userId = _userService.GetProperUserId(User).Value;
var result = await _createProjectCommand.CreateAsync(createRequest.ToProject(organizationId), userId);
return new ProjectResponseModel(result);
}

View File

@@ -2,9 +2,13 @@
using Bit.Api.SecretsManager.Models.Request;
using Bit.Api.SecretsManager.Models.Response;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Identity;
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -15,23 +19,32 @@ namespace Bit.Api.SecretsManager.Controllers;
public class SecretsController : Controller
{
private readonly ICurrentContext _currentContext;
private readonly IProjectRepository _projectRepository;
private readonly ISecretRepository _secretRepository;
private readonly ICreateSecretCommand _createSecretCommand;
private readonly IUpdateSecretCommand _updateSecretCommand;
private readonly IDeleteSecretCommand _deleteSecretCommand;
private readonly IUserService _userService;
private readonly IEventService _eventService;
public SecretsController(
ICurrentContext currentContext,
IProjectRepository projectRepository,
ISecretRepository secretRepository,
ICreateSecretCommand createSecretCommand,
IUpdateSecretCommand updateSecretCommand,
IDeleteSecretCommand deleteSecretCommand)
IDeleteSecretCommand deleteSecretCommand,
IUserService userService,
IEventService eventService)
{
_currentContext = currentContext;
_projectRepository = projectRepository;
_secretRepository = secretRepository;
_createSecretCommand = createSecretCommand;
_updateSecretCommand = updateSecretCommand;
_deleteSecretCommand = deleteSecretCommand;
_userService = userService;
_eventService = eventService;
}
[HttpGet("organizations/{organizationId}/secrets")]
@@ -42,7 +55,12 @@ public class SecretsController : Controller
throw new NotFoundException();
}
var secrets = await _secretRepository.GetManyByOrganizationIdAsync(organizationId);
var userId = _userService.GetProperUserId(User).Value;
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var secrets = await _secretRepository.GetManyByOrganizationIdAsync(organizationId, userId, accessClient);
return new SecretWithProjectsListResponseModel(secrets);
}
@@ -54,7 +72,8 @@ public class SecretsController : Controller
throw new NotFoundException();
}
var result = await _createSecretCommand.CreateAsync(createRequest.ToSecret(organizationId));
var userId = _userService.GetProperUserId(User).Value;
var result = await _createSecretCommand.CreateAsync(createRequest.ToSecret(organizationId), userId);
return new SecretResponseModel(result);
}
@@ -62,34 +81,81 @@ public class SecretsController : Controller
public async Task<SecretResponseModel> GetAsync([FromRoute] Guid id)
{
var secret = await _secretRepository.GetByIdAsync(id);
if (secret == null)
if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId))
{
throw new NotFoundException();
}
if (!await UserHasReadAccessToSecret(secret))
{
throw new NotFoundException();
}
if (_currentContext.ClientType == ClientType.ServiceAccount)
{
var userId = _userService.GetProperUserId(User).Value;
await _eventService.LogServiceAccountSecretEventAsync(userId, secret, EventType.Secret_Retrieved);
}
return new SecretResponseModel(secret);
}
[HttpGet("projects/{projectId}/secrets")]
public async Task<SecretWithProjectsListResponseModel> GetSecretsByProjectAsync([FromRoute] Guid projectId)
{
var secrets = await _secretRepository.GetManyByProjectIdAsync(projectId);
var responses = secrets.Select(s => new SecretResponseModel(s));
var project = await _projectRepository.GetByIdAsync(projectId);
if (project == null || !_currentContext.AccessSecretsManager(project.OrganizationId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User).Value;
var orgAdmin = await _currentContext.OrganizationAdmin(project.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var secrets = await _secretRepository.GetManyByProjectIdAsync(projectId, userId, accessClient);
return new SecretWithProjectsListResponseModel(secrets);
}
[HttpPut("secrets/{id}")]
public async Task<SecretResponseModel> UpdateAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest)
public async Task<SecretResponseModel> UpdateSecretAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest)
{
var result = await _updateSecretCommand.UpdateAsync(updateRequest.ToSecret(id));
var userId = _userService.GetProperUserId(User).Value;
var secret = updateRequest.ToSecret(id);
var result = await _updateSecretCommand.UpdateAsync(secret, userId);
return new SecretResponseModel(result);
}
// TODO Once permissions are setup for Secrets Manager need to enforce them on delete.
[HttpPost("secrets/delete")]
public async Task<ListResponseModel<BulkDeleteResponseModel>> BulkDeleteAsync([FromBody] List<Guid> ids)
{
var results = await _deleteSecretCommand.DeleteSecrets(ids);
var userId = _userService.GetProperUserId(User).Value;
var results = await _deleteSecretCommand.DeleteSecrets(ids, userId);
var responses = results.Select(r => new BulkDeleteResponseModel(r.Item1.Id, r.Item2));
return new ListResponseModel<BulkDeleteResponseModel>(responses);
}
public async Task<bool> UserHasReadAccessToSecret(Secret secret)
{
var userId = _userService.GetProperUserId(User).Value;
var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var hasAccess = orgAdmin;
if (secret.Projects?.Count > 0)
{
Guid projectId = secret.Projects.FirstOrDefault().Id;
hasAccess = accessClient switch
{
AccessClientType.NoAccessCheck => true,
AccessClientType.User => await _projectRepository.UserHasReadAccessToProject(projectId, userId),
AccessClientType.ServiceAccount => await _projectRepository.ServiceAccountHasReadAccessToProject(projectId, userId),
_ => false,
};
}
return hasAccess;
}
}

View File

@@ -0,0 +1,66 @@
using Bit.Api.SecretsManager.Models.Request;
using Bit.Api.SecretsManager.Models.Response;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.Porting.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.SecretsManager.Controllers;
[SecretsManager]
public class SecretsManagerPortingController : Controller
{
private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository;
private readonly IUserService _userService;
private readonly IImportCommand _importCommand;
private readonly ICurrentContext _currentContext;
public SecretsManagerPortingController(ISecretRepository secretRepository, IProjectRepository projectRepository, IUserService userService, IImportCommand importCommand, ICurrentContext currentContext)
{
_secretRepository = secretRepository;
_projectRepository = projectRepository;
_userService = userService;
_importCommand = importCommand;
_currentContext = currentContext;
}
[HttpGet("sm/{organizationId}/export")]
public async Task<SMExportResponseModel> Export([FromRoute] Guid organizationId, [FromRoute] string format = "json")
{
if (!await _currentContext.OrganizationAdmin(organizationId))
{
throw new UnauthorizedAccessException();
}
var userId = _userService.GetProperUserId(User).Value;
var projects = await _projectRepository.GetManyByOrganizationIdAsync(organizationId, userId, AccessClientType.NoAccessCheck);
var secrets = await _secretRepository.GetManyByOrganizationIdAsync(organizationId, userId, AccessClientType.NoAccessCheck);
if (projects == null && secrets == null)
{
throw new NotFoundException();
}
return new SMExportResponseModel(projects, secrets);
}
[HttpPost("sm/{organizationId}/import")]
public async Task Import([FromRoute] Guid organizationId, [FromBody] SMImportRequestModel importRequest)
{
if (!await _currentContext.OrganizationAdmin(organizationId))
{
throw new UnauthorizedAccessException();
}
if (importRequest.Projects?.Count() > 1000 || importRequest.Secrets?.Count() > 6000)
{
throw new BadRequestException("You cannot import this much data at once, the limit is 1000 projects and 6000 secrets.");
}
await _importCommand.ImportAsync(organizationId, importRequest.ToSMImport());
}
}

View File

@@ -0,0 +1,80 @@
using Bit.Api.SecretsManager.Models.Response;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.Trash.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.SecretsManager.Controllers;
[SecretsManager]
[Authorize("secrets")]
public class TrashController : Controller
{
private readonly ICurrentContext _currentContext;
private readonly ISecretRepository _secretRepository;
private readonly IEmptyTrashCommand _emptyTrashCommand;
private readonly IRestoreTrashCommand _restoreTrashCommand;
public TrashController(
ICurrentContext currentContext,
ISecretRepository secretRepository,
IEmptyTrashCommand emptyTrashCommand,
IRestoreTrashCommand restoreTrashCommand)
{
_currentContext = currentContext;
_secretRepository = secretRepository;
_emptyTrashCommand = emptyTrashCommand;
_restoreTrashCommand = restoreTrashCommand;
}
[HttpGet("secrets/{organizationId}/trash")]
public async Task<SecretWithProjectsListResponseModel> ListByOrganizationAsync(Guid organizationId)
{
if (!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
if (!await _currentContext.OrganizationAdmin(organizationId))
{
throw new UnauthorizedAccessException();
}
var secrets = await _secretRepository.GetManyByOrganizationIdInTrashAsync(organizationId);
return new SecretWithProjectsListResponseModel(secrets);
}
[HttpPost("secrets/{organizationId}/trash/empty")]
public async Task EmptyTrashAsync(Guid organizationId, [FromBody] List<Guid> ids)
{
if (!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
if (!await _currentContext.OrganizationAdmin(organizationId))
{
throw new UnauthorizedAccessException();
}
await _emptyTrashCommand.EmptyTrash(organizationId, ids);
}
[HttpPost("secrets/{organizationId}/trash/restore")]
public async Task RestoreTrashAsync(Guid organizationId, [FromBody] List<Guid> ids)
{
if (!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
if (!await _currentContext.OrganizationAdmin(organizationId))
{
throw new UnauthorizedAccessException();
}
await _restoreTrashCommand.RestoreTrash(organizationId, ids);
}
}

View File

@@ -19,20 +19,23 @@ namespace Bit.Api.SecretsManager.Controllers;
public class ServiceAccountsController : Controller
{
private readonly ICurrentContext _currentContext;
private readonly IUserService _userService;
private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IApiKeyRepository _apiKeyRepository;
private readonly ICreateAccessTokenCommand _createAccessTokenCommand;
private readonly ICreateServiceAccountCommand _createServiceAccountCommand;
private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IUpdateServiceAccountCommand _updateServiceAccountCommand;
private readonly IUserService _userService;
private readonly IRevokeAccessTokensCommand _revokeAccessTokensCommand;
public ServiceAccountsController(
ICurrentContext currentContext,
IUserService userService,
IServiceAccountRepository serviceAccountRepository,
IApiKeyRepository apiKeyRepository,
ICreateAccessTokenCommand createAccessTokenCommand,
IApiKeyRepository apiKeyRepository, ICreateServiceAccountCommand createServiceAccountCommand,
IUpdateServiceAccountCommand updateServiceAccountCommand)
ICreateServiceAccountCommand createServiceAccountCommand,
IUpdateServiceAccountCommand updateServiceAccountCommand,
IRevokeAccessTokensCommand revokeAccessTokensCommand)
{
_currentContext = currentContext;
_userService = userService;
@@ -40,6 +43,7 @@ public class ServiceAccountsController : Controller
_apiKeyRepository = apiKeyRepository;
_createServiceAccountCommand = createServiceAccountCommand;
_updateServiceAccountCommand = updateServiceAccountCommand;
_revokeAccessTokensCommand = revokeAccessTokensCommand;
_createAccessTokenCommand = createAccessTokenCommand;
}
@@ -129,4 +133,37 @@ public class ServiceAccountsController : Controller
var result = await _createAccessTokenCommand.CreateAsync(request.ToApiKey(id), userId);
return new AccessTokenCreationResponseModel(result);
}
[HttpPost("{id}/access-tokens/revoke")]
public async Task RevokeAccessTokensAsync(Guid id, [FromBody] RevokeAccessTokensRequest request)
{
var userId = _userService.GetProperUserId(User).Value;
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
if (serviceAccount == null)
{
throw new NotFoundException();
}
if (!_currentContext.AccessSecretsManager(serviceAccount.OrganizationId))
{
throw new NotFoundException();
}
var orgAdmin = await _currentContext.OrganizationAdmin(serviceAccount.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var hasAccess = accessClient switch
{
AccessClientType.NoAccessCheck => true,
AccessClientType.User => await _serviceAccountRepository.UserHasWriteAccessToServiceAccount(id, userId),
_ => false,
};
if (!hasAccess)
{
throw new NotFoundException();
}
await _revokeAccessTokensCommand.RevokeAsync(serviceAccount, request.Ids);
}
}

View File

@@ -13,7 +13,7 @@ public class AccessPoliciesCreateRequest
public IEnumerable<AccessPolicyRequest>? ServiceAccountAccessPolicyRequests { get; set; }
public List<BaseAccessPolicy> ToBaseAccessPoliciesForProject(Guid projectId)
public List<BaseAccessPolicy> ToBaseAccessPoliciesForProject(Guid grantedProjectId)
{
if (UserAccessPolicyRequests == null && GroupAccessPolicyRequests == null && ServiceAccountAccessPolicyRequests == null)
{
@@ -21,20 +21,77 @@ public class AccessPoliciesCreateRequest
}
var userAccessPolicies = UserAccessPolicyRequests?
.Select(x => x.ToUserProjectAccessPolicy(projectId)).ToList();
.Select(x => x.ToUserProjectAccessPolicy(grantedProjectId)).ToList();
var groupAccessPolicies = GroupAccessPolicyRequests?
.Select(x => x.ToGroupProjectAccessPolicy(projectId)).ToList();
.Select(x => x.ToGroupProjectAccessPolicy(grantedProjectId)).ToList();
var serviceAccountAccessPolicies = ServiceAccountAccessPolicyRequests?
.Select(x => x.ToServiceAccountProjectAccessPolicy(projectId)).ToList();
.Select(x => x.ToServiceAccountProjectAccessPolicy(grantedProjectId)).ToList();
var policies = new List<BaseAccessPolicy>();
if (userAccessPolicies != null) { policies.AddRange(userAccessPolicies); }
if (groupAccessPolicies != null) { policies.AddRange(groupAccessPolicies); }
if (serviceAccountAccessPolicies != null) { policies.AddRange(serviceAccountAccessPolicies); }
if (userAccessPolicies != null)
{
policies.AddRange(userAccessPolicies);
}
if (groupAccessPolicies != null)
{
policies.AddRange(groupAccessPolicies);
}
if (serviceAccountAccessPolicies != null)
{
policies.AddRange(serviceAccountAccessPolicies);
}
return policies;
}
public List<BaseAccessPolicy> ToBaseAccessPoliciesForServiceAccount(Guid grantedServiceAccountId)
{
if (UserAccessPolicyRequests == null && GroupAccessPolicyRequests == null)
{
throw new BadRequestException("No creation requests provided.");
}
var userAccessPolicies = UserAccessPolicyRequests?
.Select(x => x.ToUserServiceAccountAccessPolicy(grantedServiceAccountId)).ToList();
var groupAccessPolicies = GroupAccessPolicyRequests?
.Select(x => x.ToGroupServiceAccountAccessPolicy(grantedServiceAccountId)).ToList();
var policies = new List<BaseAccessPolicy>();
if (userAccessPolicies != null)
{
policies.AddRange(userAccessPolicies);
}
if (groupAccessPolicies != null)
{
policies.AddRange(groupAccessPolicies);
}
return policies;
}
public int Count()
{
var total = 0;
if (UserAccessPolicyRequests != null)
{
total += UserAccessPolicyRequests.Count();
}
if (GroupAccessPolicyRequests != null)
{
total += GroupAccessPolicyRequests.Count();
}
if (ServiceAccountAccessPolicyRequests != null)
{
total += ServiceAccountAccessPolicyRequests.Count();
}
return total;
}
}
public class AccessPolicyRequest
@@ -74,4 +131,22 @@ public class AccessPolicyRequest
Read = Read,
Write = Write
};
public UserServiceAccountAccessPolicy ToUserServiceAccountAccessPolicy(Guid id) =>
new()
{
OrganizationUserId = GranteeId,
GrantedServiceAccountId = id,
Read = Read,
Write = Write
};
public GroupServiceAccountAccessPolicy ToGroupServiceAccountAccessPolicy(Guid id) =>
new()
{
GroupId = GranteeId,
GrantedServiceAccountId = id,
Read = Read,
Write = Write
};
}

View File

@@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.SecretsManager.Entities;
namespace Bit.Api.SecretsManager.Models.Request;
public class GrantedAccessPolicyRequest
{
[Required]
public Guid GrantedId { get; set; }
[Required]
public bool Read { get; set; }
[Required]
public bool Write { get; set; }
public ServiceAccountProjectAccessPolicy ToServiceAccountProjectAccessPolicy(Guid serviceAccountId) =>
new()
{
ServiceAccountId = serviceAccountId,
GrantedProjectId = GrantedId,
Read = Read,
Write = Write,
};
}

View File

@@ -0,0 +1,7 @@
using System.ComponentModel.DataAnnotations;
public class RevokeAccessTokensRequest
{
[Required]
public Guid[] Ids { get; set; }
}

View File

@@ -0,0 +1,70 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.SecretsManager.Commands.Porting;
using Bit.Core.Utilities;
namespace Bit.Api.SecretsManager.Models.Request;
public class SMImportRequestModel
{
public IEnumerable<InnerProjectImportRequestModel> Projects { get; set; }
public IEnumerable<InnerSecretImportRequestModel> Secrets { get; set; }
public class InnerProjectImportRequestModel
{
public InnerProjectImportRequestModel() { }
[Required]
public Guid Id { get; set; }
[Required]
[EncryptedString]
[EncryptedStringLength(1000)]
public string Name { get; set; }
}
public class InnerSecretImportRequestModel
{
public InnerSecretImportRequestModel() { }
[Required]
public Guid Id { get; set; }
[Required]
[EncryptedString]
[EncryptedStringLength(1000)]
public string Key { get; set; }
[Required]
[EncryptedString]
[EncryptedStringLength(1000)]
public string Value { get; set; }
[Required]
[EncryptedString]
[EncryptedStringLength(1000)]
public string Note { get; set; }
[Required]
public IEnumerable<Guid> ProjectIds { get; set; }
}
public SMImport ToSMImport()
{
return new SMImport
{
Projects = Projects?.Select(p => new SMImport.InnerProject
{
Id = p.Id,
Name = p.Name,
}),
Secrets = Secrets?.Select(s => new SMImport.InnerSecret
{
Id = s.Id,
Key = s.Key,
Value = s.Value,
Note = s.Note,
ProjectIds = s.ProjectIds,
}),
};
}
}

View File

@@ -1,4 +1,5 @@
#nullable enable
using Bit.Core.Entities;
using Bit.Core.Models.Api;
using Bit.Core.SecretsManager.Entities;
@@ -20,6 +21,11 @@ public abstract class BaseAccessPolicyResponseModel : ResponseModel
public bool Write { get; set; }
public DateTime CreationDate { get; set; }
public DateTime RevisionDate { get; set; }
public string? GetUserDisplayName(User? user)
{
return string.IsNullOrWhiteSpace(user?.Name) ? user?.Email : user?.Name;
}
}
public class UserProjectAccessPolicyResponseModel : BaseAccessPolicyResponseModel
@@ -30,7 +36,7 @@ public class UserProjectAccessPolicyResponseModel : BaseAccessPolicyResponseMode
{
OrganizationUserId = accessPolicy.OrganizationUserId;
GrantedProjectId = accessPolicy.GrantedProjectId;
OrganizationUserName = accessPolicy.User?.Name;
OrganizationUserName = GetUserDisplayName(accessPolicy.User);
}
public UserProjectAccessPolicyResponseModel() : base(new UserProjectAccessPolicy(), _objectName)
@@ -51,7 +57,7 @@ public class UserServiceAccountAccessPolicyResponseModel : BaseAccessPolicyRespo
{
OrganizationUserId = accessPolicy.OrganizationUserId;
GrantedServiceAccountId = accessPolicy.GrantedServiceAccountId;
OrganizationUserName = accessPolicy.User?.Name;
OrganizationUserName = GetUserDisplayName(accessPolicy.User);
}
public UserServiceAccountAccessPolicyResponseModel() : base(new UserServiceAccountAccessPolicy(), _objectName)
@@ -115,6 +121,7 @@ public class ServiceAccountProjectAccessPolicyResponseModel : BaseAccessPolicyRe
ServiceAccountId = accessPolicy.ServiceAccountId;
GrantedProjectId = accessPolicy.GrantedProjectId;
ServiceAccountName = accessPolicy.ServiceAccount?.Name;
GrantedProjectName = accessPolicy.GrantedProject?.Name;
}
public ServiceAccountProjectAccessPolicyResponseModel()
@@ -125,4 +132,5 @@ public class ServiceAccountProjectAccessPolicyResponseModel : BaseAccessPolicyRe
public Guid? ServiceAccountId { get; set; }
public string? ServiceAccountName { get; set; }
public Guid? GrantedProjectId { get; set; }
public string? GrantedProjectName { get; set; }
}

View File

@@ -0,0 +1,75 @@
using Bit.Core.Entities;
using Bit.Core.Models.Api;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.SecretsManager.Entities;
namespace Bit.Api.SecretsManager.Models.Response;
public class PotentialGranteeResponseModel : ResponseModel
{
private const string _objectName = "potentialGrantee";
public PotentialGranteeResponseModel(Group group)
: base(_objectName)
{
if (group == null)
{
throw new ArgumentNullException(nameof(group));
}
Id = group.Id.ToString();
Name = group.Name;
Type = "group";
}
public PotentialGranteeResponseModel(OrganizationUserUserDetails user)
: base(_objectName)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
Id = user.Id.ToString();
Name = user.Name;
Email = user.Email;
Type = "user";
}
public PotentialGranteeResponseModel(ServiceAccount serviceAccount)
: base(_objectName)
{
if (serviceAccount == null)
{
throw new ArgumentNullException(nameof(serviceAccount));
}
Id = serviceAccount.Id.ToString();
Name = serviceAccount.Name;
Type = "serviceAccount";
}
public PotentialGranteeResponseModel(Project project)
: base(_objectName)
{
if (project == null)
{
throw new ArgumentNullException(nameof(project));
}
Id = project.Id.ToString();
Name = project.Name;
Type = "project";
}
public PotentialGranteeResponseModel() : base(_objectName)
{
}
public string Id { get; set; }
public string Name { get; set; }
public string Type { get; set; }
public string? Email { get; set; }
}

View File

@@ -11,6 +11,7 @@ public class ProjectAccessPoliciesResponseModel : ResponseModel
: base(_objectName)
{
foreach (var baseAccessPolicy in baseAccessPolicies)
{
switch (baseAccessPolicy)
{
case UserProjectAccessPolicy accessPolicy:
@@ -24,6 +25,7 @@ public class ProjectAccessPoliciesResponseModel : ResponseModel
new ServiceAccountProjectAccessPolicyResponseModel(accessPolicy));
break;
}
}
}
public ProjectAccessPoliciesResponseModel() : base(_objectName)

View File

@@ -0,0 +1,46 @@
using Bit.Core.Models.Api;
using Bit.Core.SecretsManager.Entities;
namespace Bit.Api.SecretsManager.Models.Response;
public class SMExportResponseModel : ResponseModel
{
public SMExportResponseModel(IEnumerable<Project> projects, IEnumerable<Secret> secrets, string obj = "SecretsManagerExportResponseModel") : base(obj)
{
Secrets = secrets?.Select(s => new InnerSecretExportResponseModel(s));
Projects = projects?.Select(p => new InnerProjectExportResponseModel(p));
}
public IEnumerable<InnerProjectExportResponseModel> Projects { get; set; }
public IEnumerable<InnerSecretExportResponseModel> Secrets { get; set; }
public class InnerProjectExportResponseModel
{
public InnerProjectExportResponseModel(Project project)
{
Id = project.Id;
Name = project.Name;
}
public Guid Id { get; set; }
public string Name { get; set; }
}
public class InnerSecretExportResponseModel
{
public InnerSecretExportResponseModel(Secret secret)
{
Id = secret.Id;
Key = secret.Key;
Value = secret.Value;
Note = secret.Note;
ProjectIds = secret.Projects?.Select(p => p.Id);
}
public Guid Id { get; set; }
public string Key { get; set; }
public string Value { get; set; }
public string Note { get; set; }
public IEnumerable<Guid> ProjectIds { get; set; }
}
}

View File

@@ -0,0 +1,50 @@
using Bit.Core.Models.Api;
using Bit.Core.SecretsManager.Commands.Porting;
namespace Bit.Api.SecretsManager.Models.Response;
public class SMImportResponseModel : ResponseModel
{
public SMImportResponseModel(SMImport import, string obj = "SecretsManagerImportResponseModel") : base(obj)
{
Projects = import.Projects?.Select(p => new InnerProjectImportResponseModel(p));
Secrets = import.Secrets?.Select(s => new InnerSecretImportResponseModel(s));
}
public IEnumerable<InnerProjectImportResponseModel> Projects { get; set; }
public IEnumerable<InnerSecretImportResponseModel> Secrets { get; set; }
public class InnerProjectImportResponseModel
{
public InnerProjectImportResponseModel() { }
public InnerProjectImportResponseModel(SMImport.InnerProject project)
{
Id = project.Id;
Name = project.Name;
}
public Guid Id { get; set; }
public string Name { get; set; }
}
public class InnerSecretImportResponseModel
{
public InnerSecretImportResponseModel() { }
public InnerSecretImportResponseModel(SMImport.InnerSecret secret)
{
Id = secret.Id;
Key = secret.Key;
Value = secret.Value;
Note = secret.Note;
ProjectIds = secret.ProjectIds;
}
public Guid Id { get; set; }
public string Key { get; set; }
public string Value { get; set; }
public string Note { get; set; }
public IEnumerable<Guid> ProjectIds { get; set; }
}
}

View File

@@ -0,0 +1,39 @@
using Bit.Core.Models.Api;
using Bit.Core.SecretsManager.Entities;
namespace Bit.Api.SecretsManager.Models.Response;
public class ServiceAccountAccessPoliciesResponseModel : ResponseModel
{
private const string _objectName = "serviceAccountAccessPolicies";
public ServiceAccountAccessPoliciesResponseModel(IEnumerable<BaseAccessPolicy> baseAccessPolicies)
: base(_objectName)
{
if (baseAccessPolicies == null)
{
return;
}
foreach (var baseAccessPolicy in baseAccessPolicies)
{
switch (baseAccessPolicy)
{
case UserServiceAccountAccessPolicy accessPolicy:
UserAccessPolicies.Add(new UserServiceAccountAccessPolicyResponseModel(accessPolicy));
break;
case GroupServiceAccountAccessPolicy accessPolicy:
GroupAccessPolicies.Add(new GroupServiceAccountAccessPolicyResponseModel(accessPolicy));
break;
}
}
}
public ServiceAccountAccessPoliciesResponseModel() : base(_objectName)
{
}
public List<UserServiceAccountAccessPolicyResponseModel> UserAccessPolicies { get; set; } = new();
public List<GroupServiceAccountAccessPolicyResponseModel> GroupAccessPolicies { get; set; } = new();
}

View File

@@ -92,6 +92,11 @@ public class ExceptionHandlerFilterAttribute : ExceptionFilterAttribute
errorMessage = "Unauthorized.";
context.HttpContext.Response.StatusCode = 401;
}
else if (exception is ConflictException)
{
errorMessage = exception.Message;
context.HttpContext.Response.StatusCode = 409;
}
else if (exception is AggregateException aggregateException)
{
context.HttpContext.Response.StatusCode = 400;

View File

@@ -85,8 +85,8 @@
},
"Azure.Core": {
"type": "Transitive",
"resolved": "1.24.0",
"contentHash": "+/qI1j2oU1S4/nvxb2k/wDsol00iGf1AyJX5g3epV7eOpQEP/2xcgh/cxgKMeFgn3U2fmgSiBnQZdkV+l5y0Uw==",
"resolved": "1.25.0",
"contentHash": "X8Dd4sAggS84KScWIjEbFAdt2U1KDolQopTPoHVubG2y3CM54f9l6asVrP5Uy384NWXjsspPYaJgz5xHc+KvTA==",
"dependencies": {
"Microsoft.Bcl.AsyncInterfaces": "1.1.1",
"System.Diagnostics.DiagnosticSource": "4.6.0",
@@ -123,28 +123,28 @@
},
"Azure.Storage.Blobs": {
"type": "Transitive",
"resolved": "12.11.0",
"contentHash": "50eRjIhY7Q1JN7kT2MSawDKCcwSb7uRZUkz00P/BLjSg47gm2hxUYsnJPyvzCHntYMbOWzrvaVQTwYwXabaR5Q==",
"resolved": "12.14.1",
"contentHash": "DvRBWUDMB2LjdRbsBNtz/LiVIYk56hqzSooxx4uq4rCdLj2M+7Vvoa1r+W35Dz6ZXL6p+SNcgEae3oZ+CkPfow==",
"dependencies": {
"Azure.Storage.Common": "12.10.0",
"Azure.Storage.Common": "12.13.0",
"System.Text.Json": "4.7.2"
}
},
"Azure.Storage.Common": {
"type": "Transitive",
"resolved": "12.10.0",
"contentHash": "vYkHGzUkdZTace/cDPZLG+Mh/EoPqQuGxDIBOau9D+XWoDPmuUFGk325aXplkFE4JFGpSwoytNYzk/qBCaiHqg==",
"resolved": "12.13.0",
"contentHash": "jDv8xJWeZY2Er9zA6QO25BiGolxg87rItt9CwAp7L/V9EPJeaz8oJydaNL9Wj0+3ncceoMgdiyEv66OF8YUwWQ==",
"dependencies": {
"Azure.Core": "1.22.0",
"Azure.Core": "1.25.0",
"System.IO.Hashing": "6.0.0"
}
},
"Azure.Storage.Queues": {
"type": "Transitive",
"resolved": "12.9.0",
"contentHash": "jDiyHtsCUCrWNvZW7SjJnJb46UhpdgQrWCbL8aWpapDHlq9LvbvxYpfLh4dfKAz09QiTznLMIU3i+md9+7GzqQ==",
"resolved": "12.12.0",
"contentHash": "PwrfymLYFmmOt6A0vMiDVhBV7RoOAKftzzvrbSM3W9cJKpkxAg57AhM7/wbNb3P8Uq0B73lBurkFiFzWK9PXHg==",
"dependencies": {
"Azure.Storage.Common": "12.10.0",
"Azure.Storage.Common": "12.13.0",
"System.Memory.Data": "1.0.2",
"System.Text.Json": "4.7.2"
}
@@ -171,6 +171,14 @@
"resolved": "2.0.123",
"contentHash": "RDFF4rBLLmbpi6pwkY7q/M6UXHRJEOerplDGE5jwEkP/JGJnBauAClYavNKJPW1yOTWRPIyfj4is3EaJxQXILQ=="
},
"DnsClient": {
"type": "Transitive",
"resolved": "1.7.0",
"contentHash": "2hrXR83b5g6/ZMJOA36hXp4t56yb7G1mF3Hg6IkrHxvtyaoXRn2WVdgDPN3V8+GugOlUBbTWXgPaka4dXw1QIg==",
"dependencies": {
"Microsoft.Win32.Registry": "5.0.0"
}
},
"Fido2": {
"type": "Transitive",
"resolved": "3.0.1",
@@ -2794,10 +2802,11 @@
"AspNetCoreRateLimit": "[4.0.2, )",
"AspNetCoreRateLimit.Redis": "[1.0.1, )",
"Azure.Extensions.AspNetCore.DataProtection.Blobs": "[1.2.1, )",
"Azure.Storage.Blobs": "[12.11.0, )",
"Azure.Storage.Queues": "[12.9.0, )",
"Azure.Storage.Blobs": "[12.14.1, )",
"Azure.Storage.Queues": "[12.12.0, )",
"BitPay.Light": "[1.0.1907, )",
"Braintree": "[5.12.0, )",
"DnsClient": "[1.7.0, )",
"Fido2.AspNet": "[3.0.1, )",
"Handlebars.Net": "[2.1.2, )",
"IdentityServer4": "[4.1.2, )",

View File

@@ -74,8 +74,8 @@
},
"Azure.Core": {
"type": "Transitive",
"resolved": "1.24.0",
"contentHash": "+/qI1j2oU1S4/nvxb2k/wDsol00iGf1AyJX5g3epV7eOpQEP/2xcgh/cxgKMeFgn3U2fmgSiBnQZdkV+l5y0Uw==",
"resolved": "1.25.0",
"contentHash": "X8Dd4sAggS84KScWIjEbFAdt2U1KDolQopTPoHVubG2y3CM54f9l6asVrP5Uy384NWXjsspPYaJgz5xHc+KvTA==",
"dependencies": {
"Microsoft.Bcl.AsyncInterfaces": "1.1.1",
"System.Diagnostics.DiagnosticSource": "4.6.0",
@@ -112,28 +112,28 @@
},
"Azure.Storage.Blobs": {
"type": "Transitive",
"resolved": "12.11.0",
"contentHash": "50eRjIhY7Q1JN7kT2MSawDKCcwSb7uRZUkz00P/BLjSg47gm2hxUYsnJPyvzCHntYMbOWzrvaVQTwYwXabaR5Q==",
"resolved": "12.14.1",
"contentHash": "DvRBWUDMB2LjdRbsBNtz/LiVIYk56hqzSooxx4uq4rCdLj2M+7Vvoa1r+W35Dz6ZXL6p+SNcgEae3oZ+CkPfow==",
"dependencies": {
"Azure.Storage.Common": "12.10.0",
"Azure.Storage.Common": "12.13.0",
"System.Text.Json": "4.7.2"
}
},
"Azure.Storage.Common": {
"type": "Transitive",
"resolved": "12.10.0",
"contentHash": "vYkHGzUkdZTace/cDPZLG+Mh/EoPqQuGxDIBOau9D+XWoDPmuUFGk325aXplkFE4JFGpSwoytNYzk/qBCaiHqg==",
"resolved": "12.13.0",
"contentHash": "jDv8xJWeZY2Er9zA6QO25BiGolxg87rItt9CwAp7L/V9EPJeaz8oJydaNL9Wj0+3ncceoMgdiyEv66OF8YUwWQ==",
"dependencies": {
"Azure.Core": "1.22.0",
"Azure.Core": "1.25.0",
"System.IO.Hashing": "6.0.0"
}
},
"Azure.Storage.Queues": {
"type": "Transitive",
"resolved": "12.9.0",
"contentHash": "jDiyHtsCUCrWNvZW7SjJnJb46UhpdgQrWCbL8aWpapDHlq9LvbvxYpfLh4dfKAz09QiTznLMIU3i+md9+7GzqQ==",
"resolved": "12.12.0",
"contentHash": "PwrfymLYFmmOt6A0vMiDVhBV7RoOAKftzzvrbSM3W9cJKpkxAg57AhM7/wbNb3P8Uq0B73lBurkFiFzWK9PXHg==",
"dependencies": {
"Azure.Storage.Common": "12.10.0",
"Azure.Storage.Common": "12.13.0",
"System.Memory.Data": "1.0.2",
"System.Text.Json": "4.7.2"
}
@@ -160,6 +160,14 @@
"resolved": "2.0.123",
"contentHash": "RDFF4rBLLmbpi6pwkY7q/M6UXHRJEOerplDGE5jwEkP/JGJnBauAClYavNKJPW1yOTWRPIyfj4is3EaJxQXILQ=="
},
"DnsClient": {
"type": "Transitive",
"resolved": "1.7.0",
"contentHash": "2hrXR83b5g6/ZMJOA36hXp4t56yb7G1mF3Hg6IkrHxvtyaoXRn2WVdgDPN3V8+GugOlUBbTWXgPaka4dXw1QIg==",
"dependencies": {
"Microsoft.Win32.Registry": "5.0.0"
}
},
"Fido2": {
"type": "Transitive",
"resolved": "3.0.1",
@@ -3245,10 +3253,11 @@
"AspNetCoreRateLimit": "[4.0.2, )",
"AspNetCoreRateLimit.Redis": "[1.0.1, )",
"Azure.Extensions.AspNetCore.DataProtection.Blobs": "[1.2.1, )",
"Azure.Storage.Blobs": "[12.11.0, )",
"Azure.Storage.Queues": "[12.9.0, )",
"Azure.Storage.Blobs": "[12.14.1, )",
"Azure.Storage.Queues": "[12.12.0, )",
"BitPay.Light": "[1.0.1907, )",
"Braintree": "[5.12.0, )",
"DnsClient": "[1.7.0, )",
"Fido2.AspNet": "[3.0.1, )",
"Handlebars.Net": "[2.1.2, )",
"IdentityServer4": "[4.1.2, )",

View File

@@ -35,6 +35,7 @@ public class CurrentContext : ICurrentContext
public virtual string ClientId { get; set; }
public virtual Version ClientVersion { get; set; }
public virtual ClientType ClientType { get; set; }
public virtual Guid? ServiceAccountOrganizationId { get; set; }
public CurrentContext(IProviderUserRepository providerUserRepository)
{
@@ -146,6 +147,11 @@ public class CurrentContext : ICurrentContext
ClientType = c;
}
if (ClientType == ClientType.ServiceAccount)
{
ServiceAccountOrganizationId = new Guid(GetClaimValue(claimsDict, Claims.Organization));
}
DeviceIdentifier = GetClaimValue(claimsDict, Claims.Device);
Organizations = GetOrganizations(claimsDict, orgApi);
@@ -445,6 +451,11 @@ public class CurrentContext : ICurrentContext
public bool AccessSecretsManager(Guid orgId)
{
if (ServiceAccountOrganizationId.HasValue && ServiceAccountOrganizationId.Value == orgId)
{
return true;
}
return Organizations?.Any(o => o.Id == orgId && o.AccessSecretsManager) ?? false;
}

View File

@@ -24,9 +24,10 @@
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.0.150" />
<PackageReference Include="AWSSDK.SQS" Version="3.7.2.47" />
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.2.1" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.11.0" />
<PackageReference Include="Azure.Storage.Queues" Version="12.9.0" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.14.1" />
<PackageReference Include="Azure.Storage.Queues" Version="12.12.0" />
<PackageReference Include="BitPay.Light" Version="1.0.1907" />
<PackageReference Include="DnsClient" Version="1.7.0" />
<PackageReference Include="Fido2.AspNet" Version="3.0.1" />
<PackageReference Include="Handlebars.Net" Version="2.1.2" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />

View File

@@ -28,6 +28,9 @@ public class Event : ITableObject<Guid>, IEvent
IpAddress = e.IpAddress;
ActingUserId = e.ActingUserId;
SystemUser = e.SystemUser;
DomainName = e.DomainName;
SecretId = e.SecretId;
ServiceAccountId = e.ServiceAccountId;
}
public Guid Id { get; set; }
@@ -49,6 +52,9 @@ public class Event : ITableObject<Guid>, IEvent
public string IpAddress { get; set; }
public Guid? ActingUserId { get; set; }
public EventSystemUser? SystemUser { get; set; }
public string DomainName { get; set; }
public Guid? SecretId { get; set; }
public Guid? ServiceAccountId { get; set; }
public void SetNewId()
{

View File

@@ -2,6 +2,7 @@
using System.Text.Json;
using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.Models.Business;
using Bit.Core.Utilities;
namespace Bit.Core.Entities;
@@ -198,4 +199,33 @@ public class Organization : ITableObject<Guid>, ISubscriber, IStorable, IStorabl
return providers[provider];
}
public void UpdateFromLicense(OrganizationLicense license)
{
Name = license.Name;
BusinessName = license.BusinessName;
BillingEmail = license.BillingEmail;
PlanType = license.PlanType;
Seats = license.Seats;
MaxCollections = license.MaxCollections;
UseGroups = license.UseGroups;
UseDirectory = license.UseDirectory;
UseEvents = license.UseEvents;
UseTotp = license.UseTotp;
Use2fa = license.Use2fa;
UseApi = license.UseApi;
UsePolicies = license.UsePolicies;
UseSso = license.UseSso;
UseKeyConnector = license.UseKeyConnector;
UseScim = license.UseScim;
UseResetPassword = license.UseResetPassword;
SelfHost = license.SelfHost;
UsersGetPremium = license.UsersGetPremium;
UseCustomPermissions = license.UseCustomPermissions;
Plan = license.Plan;
Enabled = license.Enabled;
ExpirationDate = license.Expires;
LicenseKey = license.LicenseKey;
RevisionDate = DateTime.UtcNow;
}
}

View File

@@ -0,0 +1,48 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Utilities;
namespace Bit.Core.Entities;
public class OrganizationDomain : ITableObject<Guid>
{
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }
public string Txt { get; set; }
[MaxLength(255)]
public string DomainName { get; set; }
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
public DateTime? VerifiedDate { get; private set; }
public DateTime NextRunDate { get; private set; }
public DateTime? LastCheckedDate { get; private set; }
public int JobRunCount { get; private set; }
public void SetNewId() => Id = CoreHelpers.GenerateComb();
public void SetNextRunDate(int interval)
{
//verification can take up to 72 hours
//1st job runs after 12hrs, 2nd after 24hrs and 3rd after 36hrs
NextRunDate = JobRunCount == 0
? CreationDate.AddHours(interval)
: NextRunDate.AddHours((JobRunCount + 1) * interval);
}
public void SetJobRunCount()
{
if (JobRunCount == 3)
{
return;
}
JobRunCount++;
}
public void SetVerifiedDate()
{
VerifiedDate = DateTime.UtcNow;
}
public void SetLastCheckedDate()
{
LastCheckedDate = DateTime.UtcNow;
}
}

View File

@@ -62,7 +62,6 @@ public class User : ITableObject<Guid>, ISubscriber, IStorable, IStorableSubscri
public bool UsesKeyConnector { get; set; }
public int FailedLoginCount { get; set; }
public DateTime? LastFailedLoginDate { get; set; }
public bool UnknownDeviceVerificationEnabled { get; set; }
[MaxLength(7)]
public string AvatarColor { get; set; }
public DateTime? LastPasswordChangeDate { get; set; }

View File

@@ -48,4 +48,6 @@ public enum DeviceType : byte
SafariExtension = 20,
[Display(Name = "SDK")]
SDK = 21,
[Display(Name = "Server")]
Server = 22
}

View File

@@ -2,5 +2,6 @@
public enum EventSystemUser : byte
{
SCIM = 1
SCIM = 1,
DomainVerification = 2
}

View File

@@ -1,5 +1,6 @@
namespace Bit.Core.Enums;
// Increment by 100 for each new set of events
public enum EventType : int
{
User_LoggedIn = 1000,
@@ -75,4 +76,11 @@ public enum EventType : int
ProviderOrganization_Added = 1901,
ProviderOrganization_Removed = 1902,
ProviderOrganization_VaultAccessed = 1903,
OrganizationDomain_Added = 2000,
OrganizationDomain_Removed = 2001,
OrganizationDomain_Verified = 2002,
OrganizationDomain_NotVerified = 2003,
Secret_Retrieved = 2100,
}

View File

@@ -1,3 +1,7 @@
namespace Bit.Core.Exceptions;
public class ConflictException : Exception { }
public class ConflictException : Exception
{
public ConflictException() : base("Conflict.") { }
public ConflictException(string message) : base(message) { }
}

View File

@@ -0,0 +1,7 @@
namespace Bit.Core.Exceptions;
public class DnsQueryException : Exception
{
public DnsQueryException(string message)
: base(message) { }
}

View File

@@ -0,0 +1,10 @@
namespace Bit.Core.Exceptions;
public class DomainClaimedException : Exception
{
public DomainClaimedException()
: base("The domain is not available to be claimed.")
{
}
}

View File

@@ -0,0 +1,10 @@
namespace Bit.Core.Exceptions;
public class DomainVerifiedException : Exception
{
public DomainVerifiedException()
: base("Domain has already been verified.")
{
}
}

View File

@@ -0,0 +1,10 @@
namespace Bit.Core.Exceptions;
public class DuplicateDomainException : Exception
{
public DuplicateDomainException()
: base("A domain already exists for this organization.")
{
}
}

View File

@@ -1,19 +0,0 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
Your two-step verification code is: <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{Token}}</b>
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
Use this code to complete logging in with Bitwarden.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
This email was sent because you are logging in from a device we dont recognize. If you did not request this code, you may want to <a target="_blank" clicktracking=off href="https://bitwarden.com/help/master-password/#change-master-password" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #175DDC; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; text-decoration: underline;">change your master password</a>. You can view our tips for selecting a secure master password <a target="_blank" clicktracking=off href="https://bitwarden.com/blog/picking-the-right-password-for-your-password-manager/" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #175DDC; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; text-decoration: underline;">here</a>.
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@@ -1,7 +0,0 @@
{{#>BasicTextLayout}}
Your two-step verification code is: {{Token}}
Use this code to complete logging in with Bitwarden.
This email was sent because you are logging in from a device we dont recognize. If you did not request this code, you may want to change your master password (https://bitwarden.com/help/master-password/#change-master-password). You can view our tips for selecting a secure master password here (https://bitwarden.com/blog/picking-the-right-password-for-your-password-manager/).
{{/BasicTextLayout}}

View File

@@ -0,0 +1,27 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;" valign="top">
The domain {{DomainName}} in your Bitwarden organization could not be verified.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
Check the corresponding record in your domain host. Then reverify this domain in Bitwarden to use it for your organization.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
The domain will be removed from your organization in 7 days if it is not verified.
</td>
</tr>
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
<a href="{{{Url}}}" clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
Manage Domains
</a>
<br style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@@ -0,0 +1,10 @@
{{#>BasicTextLayout}}
The domain {{DomainName}} in your Bitwarden organization could not be verified.
Check the corresponding record in your domain host. Then reverify this domain in Bitwarden to use it for your organization.
The domain will be removed from your organization in 7 days if it is not verified.
{{Url}}
{{/BasicTextLayout}}

View File

@@ -0,0 +1,27 @@
using System.Text.Json.Serialization;
namespace Bit.Core.Models.Api.Response.Duo;
public class DuoResponseModel
{
[JsonPropertyName("stat")]
public string Stat { get; set; }
[JsonPropertyName("code")]
public int? Code { get; set; }
[JsonPropertyName("message")]
public string Message { get; set; }
[JsonPropertyName("message_detail")]
public string MessageDetail { get; set; }
[JsonPropertyName("response")]
public Response Response { get; set; }
}
public class Response
{
[JsonPropertyName("time")]
public int Time { get; set; }
}

View File

@@ -199,21 +199,42 @@ public class OrganizationLicense : ILicense
}
}
public bool CanUse(IGlobalSettings globalSettings)
public bool CanUse(IGlobalSettings globalSettings, ILicensingService licensingService, out string exception)
{
if (!Enabled || Issued > DateTime.UtcNow || Expires < DateTime.UtcNow)
{
exception = "Invalid license. Your organization is disabled or the license has expired.";
return false;
}
if (ValidLicenseVersion)
if (!ValidLicenseVersion)
{
return InstallationId == globalSettings.Installation.Id && SelfHost;
exception = $"Version {Version} is not supported.";
return false;
}
else
if (InstallationId != globalSettings.Installation.Id || !SelfHost)
{
throw new NotSupportedException($"Version {Version} is not supported.");
exception = "Invalid license. Make sure your license allows for on-premise " +
"hosting of organizations and that the installation id matches your current installation.";
return false;
}
if (LicenseType != null && LicenseType != Enums.LicenseType.Organization)
{
exception = "Premium licenses cannot be applied to an organization. "
+ "Upload this license from your personal account settings page.";
return false;
}
if (!licensingService.VerifyLicense(this))
{
exception = "Invalid license.";
return false;
}
exception = "";
return true;
}
public bool VerifyData(Organization organization, IGlobalSettings globalSettings)

View File

@@ -32,4 +32,7 @@ public class EventMessage : IEvent
public string IpAddress { get; set; }
public Guid? IdempotencyId { get; private set; } = Guid.NewGuid();
public EventSystemUser? SystemUser { get; set; }
public string DomainName { get; set; }
public Guid? SecretId { get; set; }
public Guid? ServiceAccountId { get; set; }
}

View File

@@ -27,6 +27,9 @@ public class EventTableEntity : TableEntity, IEvent
IpAddress = e.IpAddress;
ActingUserId = e.ActingUserId;
SystemUser = e.SystemUser;
DomainName = e.DomainName;
SecretId = e.SecretId;
ServiceAccountId = e.ServiceAccountId;
}
public DateTime Date { get; set; }
@@ -46,6 +49,9 @@ public class EventTableEntity : TableEntity, IEvent
public string IpAddress { get; set; }
public Guid? ActingUserId { get; set; }
public EventSystemUser? SystemUser { get; set; }
public string DomainName { get; set; }
public Guid? SecretId { get; set; }
public Guid? ServiceAccountId { get; set; }
public override IDictionary<string, EntityProperty> WriteEntity(OperationContext operationContext)
{
@@ -152,6 +158,24 @@ public class EventTableEntity : TableEntity, IEvent
});
}
if (e.OrganizationId.HasValue && e.ServiceAccountId.HasValue)
{
entities.Add(new EventTableEntity(e)
{
PartitionKey = pKey,
RowKey = $"ServiceAccountId={e.ServiceAccountId}__Date={dateKey}__Uniquifier={uniquifier}"
});
}
if (e.SecretId.HasValue)
{
entities.Add(new EventTableEntity(e)
{
PartitionKey = pKey,
RowKey = $"SecretId={e.CipherId}__Date={dateKey}__Uniquifier={uniquifier}"
});
}
return entities;
}

View File

@@ -21,4 +21,7 @@ public interface IEvent
string IpAddress { get; set; }
DateTime Date { get; set; }
EventSystemUser? SystemUser { get; set; }
string DomainName { get; set; }
Guid? SecretId { get; set; }
Guid? ServiceAccountId { get; set; }
}

View File

@@ -0,0 +1,16 @@
using Bit.Core.Enums;
namespace Bit.Core.Models.Data.Organizations;
public class OrganizationDomainSsoDetailsData
{
public Guid OrganizationId { get; set; }
public string OrganizationName { get; set; }
public string DomainName { get; set; }
public bool SsoAvailable { get; set; }
public string OrganizationIdentifier { get; set; }
public bool SsoRequired { get; set; }
public PolicyType? PolicyType { get; set; }
public DateTime? VerifiedDate { get; set; }
public bool OrganizationEnabled { get; set; }
}

View File

@@ -12,6 +12,7 @@ public class OrganizationUserUserDetails : IExternal, ITwoFactorProvidersUser
public Guid? UserId { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string AvatarColor { get; set; }
public string TwoFactorProviders { get; set; }
public bool? Premium { get; set; }
public OrganizationUserStatusType Status { get; set; }
@@ -61,11 +62,9 @@ public class OrganizationUserUserDetails : IExternal, ITwoFactorProvidersUser
return Premium.GetValueOrDefault(false);
}
public bool OccupiesOrganizationSeat
public Permissions GetPermissions()
{
get
{
return Status != OrganizationUserStatusType.Revoked;
}
return string.IsNullOrWhiteSpace(Permissions) ? null
: CoreHelpers.LoadClassFromJsonData<Permissions>(Permissions);
}
}

View File

@@ -0,0 +1,145 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Business;
using Bit.Core.Models.OrganizationConnectionConfigs;
namespace Bit.Core.Models.Data.Organizations;
public class SelfHostedOrganizationDetails : Organization
{
public int OccupiedSeatCount { get; set; }
public int CollectionCount { get; set; }
public int GroupCount { get; set; }
public IEnumerable<OrganizationUser> OrganizationUsers { get; set; }
public IEnumerable<Policy> Policies { get; set; }
public SsoConfig SsoConfig { get; set; }
public IEnumerable<OrganizationConnection> ScimConnections { get; set; }
public bool CanUseLicense(OrganizationLicense license, out string exception)
{
if (license.Seats.HasValue && OccupiedSeatCount > license.Seats.Value)
{
exception = $"Your organization currently has {OccupiedSeatCount} seats filled. " +
$"Your new license only has ({license.Seats.Value}) seats. Remove some users.";
return false;
}
if (license.MaxCollections.HasValue && CollectionCount > license.MaxCollections.Value)
{
exception = $"Your organization currently has {CollectionCount} collections. " +
$"Your new license allows for a maximum of ({license.MaxCollections.Value}) collections. " +
"Remove some collections.";
return false;
}
if (!license.UseGroups && UseGroups && GroupCount > 1)
{
exception = $"Your organization currently has {GroupCount} groups. " +
$"Your new license does not allow for the use of groups. Remove all groups.";
return false;
}
var enabledPolicyCount = Policies.Count(p => p.Enabled);
if (!license.UsePolicies && UsePolicies && enabledPolicyCount > 0)
{
exception = $"Your organization currently has {enabledPolicyCount} enabled " +
$"policies. Your new license does not allow for the use of policies. Disable all policies.";
return false;
}
if (!license.UseSso && UseSso && SsoConfig is { Enabled: true })
{
exception = $"Your organization currently has a SSO configuration. " +
$"Your new license does not allow for the use of SSO. Disable your SSO configuration.";
return false;
}
if (!license.UseKeyConnector && UseKeyConnector && SsoConfig?.Data != null &&
SsoConfig.GetData().KeyConnectorEnabled)
{
exception = $"Your organization currently has Key Connector enabled. " +
$"Your new license does not allow for the use of Key Connector. Disable your Key Connector.";
return false;
}
if (!license.UseScim && UseScim && ScimConnections != null &&
ScimConnections.Any(c => c.GetConfig<ScimConfig>() is { Enabled: true }))
{
exception = "Your new plan does not allow the SCIM feature. " +
"Disable your SCIM configuration.";
return false;
}
if (!license.UseCustomPermissions && UseCustomPermissions &&
OrganizationUsers.Any(ou => ou.Type == OrganizationUserType.Custom))
{
exception = "Your new plan does not allow the Custom Permissions feature. " +
"Disable your Custom Permissions configuration.";
return false;
}
if (!license.UseResetPassword && UseResetPassword &&
Policies.Any(p => p.Type == PolicyType.ResetPassword && p.Enabled))
{
exception = "Your new license does not allow the Password Reset feature. "
+ "Disable your Password Reset policy.";
return false;
}
exception = "";
return true;
}
public Organization ToOrganization()
{
// Any new Organization properties must be added here for them to flow through to self-hosted organizations
return new Organization
{
Id = Id,
Identifier = Identifier,
Name = Name,
BusinessName = BusinessName,
BusinessAddress1 = BusinessAddress1,
BusinessAddress2 = BusinessAddress2,
BusinessAddress3 = BusinessAddress3,
BusinessCountry = BusinessCountry,
BusinessTaxNumber = BusinessTaxNumber,
BillingEmail = BillingEmail,
Plan = Plan,
PlanType = PlanType,
Seats = Seats,
MaxCollections = MaxCollections,
UsePolicies = UsePolicies,
UseSso = UseSso,
UseKeyConnector = UseKeyConnector,
UseScim = UseScim,
UseGroups = UseGroups,
UseDirectory = UseDirectory,
UseEvents = UseEvents,
UseTotp = UseTotp,
Use2fa = Use2fa,
UseApi = UseApi,
UseResetPassword = UseResetPassword,
UseSecretsManager = UseSecretsManager,
SelfHost = SelfHost,
UsersGetPremium = UsersGetPremium,
UseCustomPermissions = UseCustomPermissions,
Storage = Storage,
MaxStorageGb = MaxStorageGb,
Gateway = Gateway,
GatewayCustomerId = GatewayCustomerId,
GatewaySubscriptionId = GatewaySubscriptionId,
ReferenceData = ReferenceData,
Enabled = Enabled,
LicenseKey = LicenseKey,
PublicKey = PublicKey,
PrivateKey = PrivateKey,
TwoFactorProviders = TwoFactorProviders,
ExpirationDate = ExpirationDate,
CreationDate = CreationDate,
RevisionDate = RevisionDate,
MaxAutoscaleSeats = MaxAutoscaleSeats,
OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling,
};
}
}

View File

@@ -0,0 +1,7 @@
namespace Bit.Core.Models.Mail;
public class OrganizationDomainUnverifiedViewModel
{
public string Url { get; set; }
public string DomainName { get; set; }
}

View File

@@ -0,0 +1,76 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;
namespace Bit.Core.OrganizationFeatures.OrganizationDomains;
public class CreateOrganizationDomainCommand : ICreateOrganizationDomainCommand
{
private readonly IOrganizationDomainRepository _organizationDomainRepository;
private readonly IEventService _eventService;
private readonly IDnsResolverService _dnsResolverService;
private readonly ILogger<VerifyOrganizationDomainCommand> _logger;
private readonly IGlobalSettings _globalSettings;
public CreateOrganizationDomainCommand(
IOrganizationDomainRepository organizationDomainRepository,
IEventService eventService,
IDnsResolverService dnsResolverService,
ILogger<VerifyOrganizationDomainCommand> logger,
IGlobalSettings globalSettings)
{
_organizationDomainRepository = organizationDomainRepository;
_eventService = eventService;
_dnsResolverService = dnsResolverService;
_logger = logger;
_globalSettings = globalSettings;
}
public async Task<OrganizationDomain> CreateAsync(OrganizationDomain organizationDomain)
{
//Domains claimed and verified by an organization cannot be claimed
var claimedDomain =
await _organizationDomainRepository.GetClaimedDomainsByDomainNameAsync(organizationDomain.DomainName);
if (claimedDomain.Any())
{
throw new ConflictException("The domain is not available to be claimed.");
}
//check for duplicate domain entry for an organization
var duplicateOrgDomain =
await _organizationDomainRepository.GetDomainByOrgIdAndDomainNameAsync(organizationDomain.OrganizationId,
organizationDomain.DomainName);
if (duplicateOrgDomain is not null)
{
throw new ConflictException("A domain already exists for this organization.");
}
try
{
if (await _dnsResolverService.ResolveAsync(organizationDomain.DomainName, organizationDomain.Txt))
{
organizationDomain.SetVerifiedDate();
}
}
catch (Exception e)
{
_logger.LogError("Error verifying Organization domain.", e);
}
organizationDomain.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval);
organizationDomain.SetLastCheckedDate();
var orgDomain = await _organizationDomainRepository.CreateAsync(organizationDomain);
await _eventService.LogOrganizationDomainEventAsync(orgDomain, EventType.OrganizationDomain_Added);
await _eventService.LogOrganizationDomainEventAsync(orgDomain,
orgDomain.VerifiedDate != null ? EventType.OrganizationDomain_Verified : EventType.OrganizationDomain_NotVerified);
return orgDomain;
}
}

View File

@@ -0,0 +1,32 @@
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
namespace Bit.Core.OrganizationFeatures.OrganizationDomains;
public class DeleteOrganizationDomainCommand : IDeleteOrganizationDomainCommand
{
private readonly IOrganizationDomainRepository _organizationDomainRepository;
private readonly IEventService _eventService;
public DeleteOrganizationDomainCommand(IOrganizationDomainRepository organizationDomainRepository,
IEventService eventService)
{
_organizationDomainRepository = organizationDomainRepository;
_eventService = eventService;
}
public async Task DeleteAsync(Guid id)
{
var domain = await _organizationDomainRepository.GetByIdAsync(id);
if (domain is null)
{
throw new NotFoundException();
}
await _organizationDomainRepository.DeleteAsync(domain);
await _eventService.LogOrganizationDomainEventAsync(domain, EventType.OrganizationDomain_Removed);
}
}

View File

@@ -0,0 +1,18 @@
using Bit.Core.Entities;
using Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.Repositories;
namespace Bit.Core.OrganizationFeatures.OrganizationDomains;
public class GetOrganizationDomainByIdQuery : IGetOrganizationDomainByIdQuery
{
private readonly IOrganizationDomainRepository _organizationDomainRepository;
public GetOrganizationDomainByIdQuery(IOrganizationDomainRepository organizationDomainRepository)
{
_organizationDomainRepository = organizationDomainRepository;
}
public async Task<OrganizationDomain> GetOrganizationDomainById(Guid id)
=> await _organizationDomainRepository.GetByIdAsync(id);
}

View File

@@ -0,0 +1,18 @@
using Bit.Core.Entities;
using Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.Repositories;
namespace Bit.Core.OrganizationFeatures.OrganizationDomains;
public class GetOrganizationDomainByOrganizationIdQuery : IGetOrganizationDomainByOrganizationIdQuery
{
private readonly IOrganizationDomainRepository _organizationDomainRepository;
public GetOrganizationDomainByOrganizationIdQuery(IOrganizationDomainRepository organizationDomainRepository)
{
_organizationDomainRepository = organizationDomainRepository;
}
public async Task<ICollection<OrganizationDomain>> GetDomainsByOrganizationId(Guid orgId)
=> await _organizationDomainRepository.GetDomainsByOrganizationIdAsync(orgId);
}

View File

@@ -0,0 +1,8 @@
using Bit.Core.Entities;
namespace Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces;
public interface ICreateOrganizationDomainCommand
{
Task<OrganizationDomain> CreateAsync(OrganizationDomain organizationDomain);
}

View File

@@ -0,0 +1,6 @@
namespace Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces;
public interface IDeleteOrganizationDomainCommand
{
Task DeleteAsync(Guid id);
}

View File

@@ -0,0 +1,8 @@
using Bit.Core.Entities;
namespace Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces;
public interface IGetOrganizationDomainByIdQuery
{
Task<OrganizationDomain> GetOrganizationDomainById(Guid id);
}

View File

@@ -0,0 +1,8 @@
using Bit.Core.Entities;
namespace Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces;
public interface IGetOrganizationDomainByOrganizationIdQuery
{
Task<ICollection<OrganizationDomain>> GetDomainsByOrganizationId(Guid orgId);
}

View File

@@ -0,0 +1,8 @@
using Bit.Core.Entities;
namespace Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces;
public interface IVerifyOrganizationDomainCommand
{
Task<OrganizationDomain> VerifyOrganizationDomain(Guid id);
}

View File

@@ -0,0 +1,73 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.Extensions.Logging;
namespace Bit.Core.OrganizationFeatures.OrganizationDomains;
public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
{
private readonly IOrganizationDomainRepository _organizationDomainRepository;
private readonly IDnsResolverService _dnsResolverService;
private readonly IEventService _eventService;
private readonly ILogger<VerifyOrganizationDomainCommand> _logger;
public VerifyOrganizationDomainCommand(
IOrganizationDomainRepository organizationDomainRepository,
IDnsResolverService dnsResolverService,
IEventService eventService,
ILogger<VerifyOrganizationDomainCommand> logger)
{
_organizationDomainRepository = organizationDomainRepository;
_dnsResolverService = dnsResolverService;
_eventService = eventService;
_logger = logger;
}
public async Task<OrganizationDomain> VerifyOrganizationDomain(Guid id)
{
var domain = await _organizationDomainRepository.GetByIdAsync(id);
if (domain is null)
{
throw new NotFoundException();
}
if (domain.VerifiedDate is not null)
{
domain.SetLastCheckedDate();
await _organizationDomainRepository.ReplaceAsync(domain);
throw new ConflictException("Domain has already been verified.");
}
var claimedDomain =
await _organizationDomainRepository.GetClaimedDomainsByDomainNameAsync(domain.DomainName);
if (claimedDomain.Any())
{
domain.SetLastCheckedDate();
await _organizationDomainRepository.ReplaceAsync(domain);
throw new ConflictException("The domain is not available to be claimed.");
}
try
{
if (await _dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt))
{
domain.SetVerifiedDate();
}
}
catch (Exception e)
{
_logger.LogError("Error verifying Organization domain. {errorMessage}", e.Message);
}
domain.SetLastCheckedDate();
await _organizationDomainRepository.ReplaceAsync(domain);
await _eventService.LogOrganizationDomainEventAsync(domain,
domain.VerifiedDate != null ? EventType.OrganizationDomain_Verified : EventType.OrganizationDomain_NotVerified);
return domain;
}
}

View File

@@ -32,7 +32,7 @@ public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuer
throw new BadRequestException("Invalid installation id");
}
var subInfo = await _paymentService.GetSubscriptionAsync(organization);
return new OrganizationLicense(organization, subInfo, installationId, _licensingService, version);
var subscriptionInfo = await _paymentService.GetSubscriptionAsync(organization);
return new OrganizationLicense(organization, subscriptionInfo, installationId, _licensingService, version);
}
}

View File

@@ -0,0 +1,13 @@
#nullable enable
using Bit.Core.Entities;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations;
namespace Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
public interface IUpdateOrganizationLicenseCommand
{
Task UpdateLicenseAsync(SelfHostedOrganizationDetails selfHostedOrganization,
OrganizationLicense license, Organization? currentOrganizationUsingLicenseKey);
}

View File

@@ -0,0 +1,66 @@
#nullable enable
using System.Text.Json;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
namespace Bit.Core.OrganizationFeatures.OrganizationLicenses;
public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseCommand
{
private readonly ILicensingService _licensingService;
private readonly IGlobalSettings _globalSettings;
private readonly IOrganizationService _organizationService;
public UpdateOrganizationLicenseCommand(
ILicensingService licensingService,
IGlobalSettings globalSettings,
IOrganizationService organizationService)
{
_licensingService = licensingService;
_globalSettings = globalSettings;
_organizationService = organizationService;
}
public async Task UpdateLicenseAsync(SelfHostedOrganizationDetails selfHostedOrganization,
OrganizationLicense license, Organization? currentOrganizationUsingLicenseKey)
{
if (currentOrganizationUsingLicenseKey != null && currentOrganizationUsingLicenseKey.Id != selfHostedOrganization.Id)
{
throw new BadRequestException("License is already in use by another organization.");
}
var canUse = license.CanUse(_globalSettings, _licensingService, out var exception) &&
selfHostedOrganization.CanUseLicense(license, out exception);
if (!canUse)
{
throw new BadRequestException(exception);
}
await WriteLicenseFileAsync(selfHostedOrganization, license);
await UpdateOrganizationAsync(selfHostedOrganization, license);
}
private async Task WriteLicenseFileAsync(Organization organization, OrganizationLicense license)
{
var dir = $"{_globalSettings.LicenseDirectory}/organization";
Directory.CreateDirectory(dir);
await using var fs = new FileStream(Path.Combine(dir, $"{organization.Id}.json"), FileMode.Create);
await JsonSerializer.SerializeAsync(fs, license, JsonHelpers.Indented);
}
private async Task UpdateOrganizationAsync(SelfHostedOrganizationDetails selfHostedOrganizationDetails, OrganizationLicense license)
{
var organization = selfHostedOrganizationDetails.ToOrganization();
organization.UpdateFromLicense(license);
await _organizationService.ReplaceAndUpdateCacheAsync(organization);
}
}

View File

@@ -7,6 +7,8 @@ using Bit.Core.OrganizationFeatures.OrganizationCollections;
using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationConnections;
using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationDomains;
using Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationLicenses;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;
@@ -34,7 +36,8 @@ public static class OrganizationServiceCollectionExtensions
services.AddOrganizationApiKeyCommandsQueries();
services.AddOrganizationCollectionCommands();
services.AddOrganizationGroupCommands();
services.AddOrganizationLicenseCommandQueries();
services.AddOrganizationLicenseCommandsQueries();
services.AddOrganizationDomainCommandsQueries();
}
private static void AddOrganizationConnectionCommands(this IServiceCollection services)
@@ -88,10 +91,20 @@ public static class OrganizationServiceCollectionExtensions
services.AddScoped<IUpdateGroupCommand, UpdateGroupCommand>();
}
private static void AddOrganizationLicenseCommandQueries(this IServiceCollection services)
private static void AddOrganizationLicenseCommandsQueries(this IServiceCollection services)
{
services.AddScoped<ICloudGetOrganizationLicenseQuery, CloudGetOrganizationLicenseQuery>();
services.AddScoped<ISelfHostedGetOrganizationLicenseQuery, SelfHostedGetOrganizationLicenseQuery>();
services.AddScoped<IUpdateOrganizationLicenseCommand, UpdateOrganizationLicenseCommand>();
}
private static void AddOrganizationDomainCommandsQueries(this IServiceCollection services)
{
services.AddScoped<ICreateOrganizationDomainCommand, CreateOrganizationDomainCommand>();
services.AddScoped<IVerifyOrganizationDomainCommand, VerifyOrganizationDomainCommand>();
services.AddScoped<IGetOrganizationDomainByIdQuery, GetOrganizationDomainByIdQuery>();
services.AddScoped<IGetOrganizationDomainByOrganizationIdQuery, GetOrganizationDomainByOrganizationIdQuery>();
services.AddScoped<IDeleteOrganizationDomainCommand, DeleteOrganizationDomainCommand>();
}
private static void AddTokenizers(this IServiceCollection services)

View File

@@ -0,0 +1,15 @@
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations;
namespace Bit.Core.Repositories;
public interface IOrganizationDomainRepository : IRepository<OrganizationDomain, Guid>
{
Task<ICollection<OrganizationDomain>> GetClaimedDomainsByDomainNameAsync(string domainName);
Task<ICollection<OrganizationDomain>> GetDomainsByOrganizationIdAsync(Guid orgId);
Task<ICollection<OrganizationDomain>> GetManyByNextRunDateAsync(DateTime date);
Task<OrganizationDomainSsoDetailsData> GetOrganizationDomainSsoDetailsAsync(string email);
Task<OrganizationDomain> GetDomainByOrgIdAndDomainNameAsync(Guid orgId, string domainName);
Task<ICollection<OrganizationDomain>> GetExpiredOrganizationDomainsAsync();
Task<bool> DeleteExpiredAsync(int expirationPeriod);
}

View File

@@ -11,4 +11,6 @@ public interface IOrganizationRepository : IRepository<Organization, Guid>
Task<ICollection<Organization>> SearchAsync(string name, string userEmail, bool? paid, int skip, int take);
Task UpdateStorageAsync(Guid id);
Task<ICollection<OrganizationAbility>> GetManyAbilitiesAsync();
Task<Organization> GetByLicenseKeyAsync(string licenseKey);
Task<SelfHostedOrganizationDetails> GetSelfHostedOrganizationDetailsById(Guid id);
}

View File

@@ -13,6 +13,7 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
Task<ICollection<OrganizationUser>> GetManyByUserAsync(Guid userId);
Task<ICollection<OrganizationUser>> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type);
Task<int> GetCountByOrganizationAsync(Guid organizationId, string email, bool onlyRegisteredUsers);
Task<int> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId);
Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails, bool onlyRegisteredUsers);
Task<OrganizationUser> GetByOrganizationAsync(Guid organizationId, Guid userId);
Task<Tuple<OrganizationUser, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id);

View File

@@ -1,8 +1,9 @@
using Bit.Core.SecretsManager.Entities;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Entities;
namespace Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
public interface ICreateAccessPoliciesCommand
{
Task<List<BaseAccessPolicy>> CreateAsync(List<BaseAccessPolicy> accessPolicies);
Task<IEnumerable<BaseAccessPolicy>> CreateManyAsync(List<BaseAccessPolicy> accessPolicies, Guid userId, AccessClientType accessType);
}

View File

@@ -2,5 +2,5 @@
public interface IDeleteAccessPolicyCommand
{
Task DeleteAsync(Guid id);
Task DeleteAsync(Guid id, Guid userId);
}

View File

@@ -4,5 +4,5 @@ namespace Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
public interface IUpdateAccessPolicyCommand
{
public Task<BaseAccessPolicy> UpdateAsync(Guid id, bool read, bool write);
public Task<BaseAccessPolicy> UpdateAsync(Guid id, bool read, bool write, Guid userId);
}

View File

@@ -0,0 +1,7 @@
namespace Bit.Core.SecretsManager.Commands.Porting.Interfaces;
public interface IImportCommand
{
Task ImportAsync(Guid organizationId, SMImport import);
SMImport AssignNewIds(SMImport import);
}

View File

@@ -0,0 +1,41 @@
namespace Bit.Core.SecretsManager.Commands.Porting;
public class SMImport
{
public IEnumerable<InnerProject> Projects { get; set; }
public IEnumerable<InnerSecret> Secrets { get; set; }
public class InnerProject
{
public InnerProject() { }
public InnerProject(Core.SecretsManager.Entities.Project project)
{
Id = project.Id;
Name = project.Name;
}
public Guid Id { get; set; }
public string Name { get; set; }
}
public class InnerSecret
{
public InnerSecret() { }
public InnerSecret(Core.SecretsManager.Entities.Secret secret)
{
Id = secret.Id;
Key = secret.Key;
Value = secret.Value;
Note = secret.Note;
ProjectIds = secret.Projects != null && secret.Projects.Any() ? secret.Projects.Select(p => p.Id) : null;
}
public Guid Id { get; set; }
public string Key { get; set; }
public string Value { get; set; }
public string Note { get; set; }
public IEnumerable<Guid> ProjectIds { get; set; }
}
}

View File

@@ -4,5 +4,5 @@ namespace Bit.Core.SecretsManager.Commands.Projects.Interfaces;
public interface ICreateProjectCommand
{
Task<Project> CreateAsync(Project project);
Task<Project> CreateAsync(Project project, Guid userId);
}

View File

@@ -4,5 +4,5 @@ namespace Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
public interface ICreateSecretCommand
{
Task<Secret> CreateAsync(Secret secret);
Task<Secret> CreateAsync(Secret secret, Guid userId);
}

View File

@@ -4,6 +4,6 @@ namespace Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
public interface IDeleteSecretCommand
{
Task<List<Tuple<Secret, string>>> DeleteSecrets(List<Guid> ids);
Task<List<Tuple<Secret, string>>> DeleteSecrets(List<Guid> ids, Guid userId);
}

View File

@@ -4,5 +4,5 @@ namespace Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
public interface IUpdateSecretCommand
{
Task<Secret> UpdateAsync(Secret secret);
Task<Secret> UpdateAsync(Secret secret, Guid userId);
}

View File

@@ -0,0 +1,8 @@
using Bit.Core.SecretsManager.Entities;
namespace Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
public interface IRevokeAccessTokensCommand
{
Task RevokeAsync(ServiceAccount serviceAccount, IEnumerable<Guid> ids);
}

View File

@@ -0,0 +1,7 @@
namespace Bit.Core.SecretsManager.Commands.Trash.Interfaces;
public interface IEmptyTrashCommand
{
Task EmptyTrash(Guid organizationId, List<Guid> ids);
}

View File

@@ -0,0 +1,6 @@
namespace Bit.Core.SecretsManager.Commands.Trash.Interfaces;
public interface IRestoreTrashCommand
{
Task RestoreTrash(Guid organizationId, List<Guid> ids);
}

View File

@@ -24,34 +24,39 @@ public abstract class BaseAccessPolicy
public class UserProjectAccessPolicy : BaseAccessPolicy
{
public Guid? OrganizationUserId { get; set; }
public Guid? GrantedProjectId { get; set; }
public User? User { get; set; }
public Guid? GrantedProjectId { get; set; }
public Project? GrantedProject { get; set; }
}
public class UserServiceAccountAccessPolicy : BaseAccessPolicy
{
public Guid? OrganizationUserId { get; set; }
public Guid? GrantedServiceAccountId { get; set; }
public User? User { get; set; }
public Guid? GrantedServiceAccountId { get; set; }
public ServiceAccount? GrantedServiceAccount { get; set; }
}
public class GroupProjectAccessPolicy : BaseAccessPolicy
{
public Guid? GroupId { get; set; }
public Guid? GrantedProjectId { get; set; }
public Group? Group { get; set; }
public Guid? GrantedProjectId { get; set; }
public Project? GrantedProject { get; set; }
}
public class GroupServiceAccountAccessPolicy : BaseAccessPolicy
{
public Guid? GroupId { get; set; }
public Guid? GrantedServiceAccountId { get; set; }
public Group? Group { get; set; }
public Guid? GrantedServiceAccountId { get; set; }
public ServiceAccount? GrantedServiceAccount { get; set; }
}
public class ServiceAccountProjectAccessPolicy : BaseAccessPolicy
{
public Guid? ServiceAccountId { get; set; }
public Guid? GrantedProjectId { get; set; }
public ServiceAccount? ServiceAccount { get; set; }
public Guid? GrantedProjectId { get; set; }
public Project? GrantedProject { get; set; }
}

View File

@@ -1,4 +1,5 @@
#nullable enable
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Entities;
namespace Bit.Core.SecretsManager.Repositories;
@@ -10,6 +11,8 @@ public interface IAccessPolicyRepository
Task<BaseAccessPolicy?> GetByIdAsync(Guid id);
Task<IEnumerable<BaseAccessPolicy>> GetManyByGrantedProjectIdAsync(Guid id);
Task<IEnumerable<BaseAccessPolicy>> GetManyByGrantedServiceAccountIdAsync(Guid id);
Task<IEnumerable<BaseAccessPolicy>> GetManyByServiceAccountIdAsync(Guid id, Guid userId,
AccessClientType accessType);
Task ReplaceAsync(BaseAccessPolicy baseAccessPolicy);
Task DeleteAsync(Guid id);
}

Some files were not shown because too many files have changed in this diff Show More