diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index 5a1491f7c..c6b083631 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -12,8 +12,6 @@ using System.Linq; using Bit.Core.Repositories; using Bit.Core.Utilities; using Bit.Core; -using System.IO; -using Newtonsoft.Json; using Bit.Core.Models.Business; using Bit.Api.Utilities; diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index 81facbc9c..c485434d7 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -296,6 +296,26 @@ namespace Bit.Api.Controllers } } + [HttpPut("{id}/license")] + [HttpPost("{id}/license")] + [SelfHosted(SelfHostedOnly = true)] + public async Task PutLicense(string id, LicenseRequestModel model) + { + var orgIdGuid = new Guid(id); + if(!_currentContext.OrganizationOwner(orgIdGuid)) + { + throw new NotFoundException(); + } + + var license = await ApiHelpers.ReadJsonFileFromBody(HttpContext, model.License); + if(license == null) + { + throw new BadRequestException("Invalid license"); + } + + await _organizationService.UpdateLicenseAsync(id, license); + } + [HttpPost("{id}/import")] public async Task Import(string id, [FromBody]ImportOrganizationUsersRequestModel model) { diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index ac2f9b099..a81b6cdbd 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -11,6 +11,8 @@ using Microsoft.AspNetCore.DataProtection; using Stripe; using Bit.Core.Enums; using Bit.Core.Models.Data; +using System.IO; +using Newtonsoft.Json; namespace Bit.Core.Services { @@ -618,6 +620,72 @@ namespace Bit.Core.Services } } + public async Task UpdateLicenseAsync(Guid organizationId, OrganizationLicense license) + { + var organization = await _organizationRepository.GetByIdAsync(organizationId); + if(organization == null) + { + throw new NotFoundException(); + } + + if(!_globalSettings.SelfHosted) + { + throw new InvalidOperationException("Licenses require self hosting."); + } + + if(license == null || !_licensingService.VerifyLicense(license) || !license.CanUse(_globalSettings.Installation.Id)) + { + throw new BadRequestException("Invalid license."); + } + + if(!license.SelfHost) + { + throw new BadRequestException("This license does not allow on-premise hosting."); + } + + if(license.Seats.HasValue && (!organization.Seats.HasValue || organization.Seats.Value > license.Seats.Value)) + { + var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id); + if(userCount >= license.Seats.Value) + { + throw new BadRequestException($"Your organization currently has {userCount} seats filled. " + + $"Your new license only has ({ license.Seats.Value}) seats. Remove some users."); + } + } + + if(license.MaxCollections.HasValue && + (!organization.MaxCollections.HasValue || organization.MaxCollections.Value > license.MaxCollections.Value)) + { + var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(organization.Id); + if(collectionCount > license.MaxCollections.Value) + { + throw new BadRequestException($"Your organization currently has {collectionCount} collections. " + + $"Your new license allows for a maximum of ({license.MaxCollections.Value}) collections. " + + "Remove some collections."); + } + } + + // TODO: groups + + var dir = $"{_globalSettings.LicenseDirectory}/organization"; + Directory.CreateDirectory(dir); + File.WriteAllText($"{dir}/{organization.Id}.json", JsonConvert.SerializeObject(license, Formatting.Indented)); + + organization.Name = license.Name; + organization.PlanType = license.PlanType; + organization.Seats = license.Seats; + organization.MaxCollections = license.MaxCollections; + organization.UseGroups = license.UseGroups; + organization.UseDirectory = license.UseDirectory; + organization.UseTotp = license.UseTotp; + organization.Plan = license.Plan; + organization.Enabled = true; + organization.ExpirationDate = license.Expires; + organization.LicenseKey = license.LicenseKey; + organization.RevisionDate = DateTime.UtcNow; + await _organizationRepository.ReplaceAsync(organization); + } + public async Task DeleteAsync(Organization organization) { if(!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))