diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index c6b083631d..23a487fa4a 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -421,7 +421,8 @@ namespace Bit.Api.Controllers { var paymentService = user.GetPaymentService(_globalSettings); var billingInfo = await paymentService.GetBillingAsync(user); - return new BillingResponseModel(user, billingInfo, _licenseService); + var license = await _userService.GenerateLicenseAsync(user, billingInfo); + return new BillingResponseModel(user, billingInfo, license); } else { diff --git a/src/Api/Controllers/LicensesController.cs b/src/Api/Controllers/LicensesController.cs new file mode 100644 index 0000000000..a3e6d51ffd --- /dev/null +++ b/src/Api/Controllers/LicensesController.cs @@ -0,0 +1,78 @@ +using Microsoft.AspNetCore.Mvc; +using Bit.Core.Services; +using Microsoft.AspNetCore.Authorization; +using Bit.Core; +using System.Threading.Tasks; +using Bit.Api.Utilities; +using Bit.Core.Models.Business; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using System; + +namespace Bit.Api.Controllers +{ + [Route("licenses")] + [Authorize("Licensing")] + [SelfHosted(NotSelfHostedOnly = true)] + public class LicensesController : Controller + { + private readonly ILicensingService _licensingService; + private readonly IUserRepository _userRepository; + private readonly IUserService _userService; + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationService _organizationService; + private readonly CurrentContext _currentContext; + + public LicensesController( + ILicensingService licensingService, + IUserRepository userRepository, + IUserService userService, + IOrganizationRepository organizationRepository, + IOrganizationService organizationService, + CurrentContext currentContext) + { + _licensingService = licensingService; + _userRepository = userRepository; + _userService = userService; + _organizationRepository = organizationRepository; + _organizationService = organizationService; + _currentContext = currentContext; + } + + [HttpGet("user/{id}")] + public async Task GetUser(string id, [FromQuery]string key) + { + var user = await _userRepository.GetByIdAsync(new Guid(id)); + if(user == null) + { + return null; + } + else if(!user.LicenseKey.Equals(key)) + { + await Task.Delay(2000); + throw new BadRequestException("Invalid license key."); + } + + var license = await _userService.GenerateLicenseAsync(user, null); + return license; + } + + [HttpGet("organization/{id}")] + public async Task GetOrganization(string id, [FromQuery]string key) + { + var org = await _organizationRepository.GetByIdAsync(new Guid(id)); + if(org == null) + { + return null; + } + else if(!org.LicenseKey.Equals(key)) + { + await Task.Delay(2000); + throw new BadRequestException("Invalid license key."); + } + + var license = await _organizationService.GenerateLicenseAsync(org, _currentContext.InstallationId.Value); + return license; + } + } +} diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 440bbf1442..a1edbaa46b 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -97,6 +97,11 @@ namespace Bit.Api policy.RequireAuthenticatedUser(); policy.RequireClaim(JwtClaimTypes.Scope, "api.push"); }); + config.AddPolicy("Licensing", policy => + { + policy.RequireAuthenticatedUser(); + policy.RequireClaim(JwtClaimTypes.Scope, "api.licensing"); + }); }); services.AddScoped(); @@ -190,7 +195,7 @@ namespace Bit.Api var options = new IdentityServerAuthenticationOptions { Authority = globalSettings.BaseServiceUri.InternalIdentity, - AllowedScopes = new string[] { "api", "api.push" }, + AllowedScopes = new string[] { "api", "api.push", "api.licensing" }, RequireHttpsMetadata = !env.IsDevelopment() && globalSettings.BaseServiceUri.InternalIdentity.StartsWith("https"), NameClaimType = ClaimTypes.Email, // Suffix until we retire the old jwt schemes. diff --git a/src/Core/IdentityServer/ApiResources.cs b/src/Core/IdentityServer/ApiResources.cs index 4e6dc1e895..acc8820c05 100644 --- a/src/Core/IdentityServer/ApiResources.cs +++ b/src/Core/IdentityServer/ApiResources.cs @@ -21,7 +21,8 @@ namespace Bit.Core.IdentityServer "orgadmin", "orguser" }), - new ApiResource("api.push", new string[] { JwtClaimTypes.Subject }) + new ApiResource("api.push", new string[] { JwtClaimTypes.Subject }), + new ApiResource("api.licensing", new string[] { JwtClaimTypes.Subject }) }; } } diff --git a/src/Core/IdentityServer/ClientStore.cs b/src/Core/IdentityServer/ClientStore.cs index 3ea4232682..2a46e9cb56 100644 --- a/src/Core/IdentityServer/ClientStore.cs +++ b/src/Core/IdentityServer/ClientStore.cs @@ -25,8 +25,7 @@ namespace Bit.Core.IdentityServer if(clientId.StartsWith("installation.")) { var idParts = clientId.Split('.'); - Guid id; - if(idParts.Length > 1 && Guid.TryParse(idParts[1], out id)) + if(idParts.Length > 1 && Guid.TryParse(idParts[1], out Guid id)) { var installation = await _installationRepository.GetByIdAsync(id); if(installation != null) @@ -36,7 +35,7 @@ namespace Bit.Core.IdentityServer ClientId = $"installation.{installation.Id}", RequireClientSecret = true, ClientSecrets = { new Secret(installation.Key.Sha256()) }, - AllowedScopes = new string[] { "api.push" }, + AllowedScopes = new string[] { "api.push", "api.licensing" }, AllowedGrantTypes = GrantTypes.ClientCredentials, AccessTokenLifetime = 3600 * 24, Enabled = installation.Enabled, diff --git a/src/Core/Models/Api/Response/BillingResponseModel.cs b/src/Core/Models/Api/Response/BillingResponseModel.cs index ae4aa4c150..b9bfeedc73 100644 --- a/src/Core/Models/Api/Response/BillingResponseModel.cs +++ b/src/Core/Models/Api/Response/BillingResponseModel.cs @@ -4,13 +4,12 @@ using System.Collections.Generic; using Bit.Core.Models.Business; using Bit.Core.Models.Table; using Bit.Core.Enums; -using Bit.Core.Services; namespace Bit.Core.Models.Api { public class BillingResponseModel : ResponseModel { - public BillingResponseModel(User user, BillingInfo billing, ILicensingService licenseService) + public BillingResponseModel(User user, BillingInfo billing, UserLicense license) : base("billing") { PaymentSource = billing.PaymentSource != null ? new BillingSource(billing.PaymentSource) : null; @@ -20,7 +19,7 @@ namespace Bit.Core.Models.Api StorageName = user.Storage.HasValue ? Utilities.CoreHelpers.ReadableBytesSize(user.Storage.Value) : null; StorageGb = user.Storage.HasValue ? Math.Round(user.Storage.Value / 1073741824D, 2) : 0; // 1 GB MaxStorageGb = user.MaxStorageGb; - License = new UserLicense(user, billing, licenseService); + License = license; Expiration = License.Expires; } diff --git a/src/Core/Models/Business/UserLicense.cs b/src/Core/Models/Business/UserLicense.cs index 354b7fd373..56433276de 100644 --- a/src/Core/Models/Business/UserLicense.cs +++ b/src/Core/Models/Business/UserLicense.cs @@ -34,6 +34,24 @@ namespace Bit.Core.Models.Business Signature = Convert.ToBase64String(licenseService.SignLicense(this)); } + public UserLicense(User user, ILicensingService licenseService) + { + LicenseKey = user.LicenseKey; + Id = user.Id; + Name = user.Name; + Email = user.Email; + Version = 1; + Premium = user.Premium; + MaxStorageGb = user.MaxStorageGb; + Issued = DateTime.UtcNow; + Expires = user.PremiumExpirationDate?.AddDays(7); + Refresh = user.PremiumExpirationDate?.Date; + Trial = false; + + Hash = Convert.ToBase64String(ComputeHash()); + Signature = Convert.ToBase64String(licenseService.SignLicense(this)); + } + public string LicenseKey { get; set; } public Guid Id { get; set; } public string Name { get; set; } diff --git a/src/Core/Services/IOrganizationService.cs b/src/Core/Services/IOrganizationService.cs index 7323407319..99ed8bce45 100644 --- a/src/Core/Services/IOrganizationService.cs +++ b/src/Core/Services/IOrganizationService.cs @@ -36,6 +36,7 @@ namespace Bit.Core.Services Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid deletingUserId); Task DeleteUserAsync(Guid organizationId, Guid userId); Task GenerateLicenseAsync(Guid organizationId, Guid installationId); + Task GenerateLicenseAsync(Organization organization, Guid installationId); Task ImportAsync(Guid organizationId, Guid importingUserId, IEnumerable groups, IEnumerable newUsers, IEnumerable removeUserExternalIds); } diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 38e17a40c3..be87399c5e 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -49,5 +49,6 @@ namespace Bit.Core.Services Task DisablePremiumAsync(Guid userId, DateTime? expirationDate); Task DisablePremiumAsync(User user, DateTime? expirationDate); Task UpdatePremiumExpirationAsync(Guid userId, DateTime? expirationDate); + Task GenerateLicenseAsync(User user, BillingInfo billingInfo = null); } } diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 6983cb66f8..91cd3bcfd5 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -1039,6 +1039,11 @@ namespace Bit.Core.Services public async Task GenerateLicenseAsync(Guid organizationId, Guid installationId) { var organization = await _organizationRepository.GetByIdAsync(organizationId); + return await GenerateLicenseAsync(organization, installationId); + } + + public async Task GenerateLicenseAsync(Organization organization, Guid installationId) + { if(organization == null) { throw new NotFoundException(); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index f4b707bb45..1517707d96 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -721,6 +721,23 @@ namespace Bit.Core.Services } } + public async Task GenerateLicenseAsync(User user, BillingInfo billingInfo = null) + { + if(user == null) + { + throw new NotFoundException(); + } + + if(billingInfo == null && user.Gateway != null) + { + var paymentService = user.GetPaymentService(_globalSettings); + billingInfo = await paymentService.GetBillingAsync(user); + } + + return billingInfo == null ? new UserLicense(user, _licenseService) : + new UserLicense(user, billingInfo, _licenseService); + } + private async Task UpdatePasswordHash(User user, string newPassword, bool validatePassword = true) { if(validatePassword)