mirror of
https://github.com/bitwarden/server.git
synced 2026-02-01 14:43:10 +08:00
* Get limited life attachment download URL This change limits url download to a 1min lifetime. This requires moving to a new container to allow for non-public blob access. Clients will have to call GetAttachmentData api function to receive the download URL. For backwards compatibility, attachment URLs are still present, but will not work for attachments stored in non-public access blobs. * Make GlobalSettings interface for testing * Test LocalAttachmentStorageService equivalence * Remove comment * Add missing globalSettings using * Simplify default attachment container * Default to attachments containe for existing methods A new upload method will be made for uploading to attachments-v2. For compatibility for clients which don't use these new methods, we need to still use the old container. The new container will be used only for new uploads * Remove Default MetaData fixture. * Keep attachments container blob-level security for all instances * Close unclosed FileStream * Favor default value for noop services
441 lines
16 KiB
C#
441 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using Bit.Admin.Models;
|
|
using Bit.Core;
|
|
using Bit.Core.Models.Table;
|
|
using Bit.Core.Repositories;
|
|
using Bit.Core.Services;
|
|
using Bit.Core.Settings;
|
|
using Bit.Core.Utilities;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Newtonsoft.Json;
|
|
|
|
namespace Bit.Admin.Controllers
|
|
{
|
|
[Authorize]
|
|
[SelfHosted(NotSelfHostedOnly = true)]
|
|
public class ToolsController : Controller
|
|
{
|
|
private readonly GlobalSettings _globalSettings;
|
|
private readonly IOrganizationRepository _organizationRepository;
|
|
private readonly IOrganizationService _organizationService;
|
|
private readonly IUserService _userService;
|
|
private readonly ITransactionRepository _transactionRepository;
|
|
private readonly IInstallationRepository _installationRepository;
|
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
|
private readonly IPaymentService _paymentService;
|
|
private readonly ITaxRateRepository _taxRateRepository;
|
|
|
|
public ToolsController(
|
|
GlobalSettings globalSettings,
|
|
IOrganizationRepository organizationRepository,
|
|
IOrganizationService organizationService,
|
|
IUserService userService,
|
|
ITransactionRepository transactionRepository,
|
|
IInstallationRepository installationRepository,
|
|
IOrganizationUserRepository organizationUserRepository,
|
|
ITaxRateRepository taxRateRepository,
|
|
IPaymentService paymentService)
|
|
{
|
|
_globalSettings = globalSettings;
|
|
_organizationRepository = organizationRepository;
|
|
_organizationService = organizationService;
|
|
_userService = userService;
|
|
_transactionRepository = transactionRepository;
|
|
_installationRepository = installationRepository;
|
|
_organizationUserRepository = organizationUserRepository;
|
|
_taxRateRepository = taxRateRepository;
|
|
_paymentService = paymentService;
|
|
}
|
|
|
|
public IActionResult ChargeBraintree()
|
|
{
|
|
return View(new ChargeBraintreeModel());
|
|
}
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> ChargeBraintree(ChargeBraintreeModel model)
|
|
{
|
|
if (!ModelState.IsValid)
|
|
{
|
|
return View(model);
|
|
}
|
|
|
|
var btGateway = new Braintree.BraintreeGateway
|
|
{
|
|
Environment = _globalSettings.Braintree.Production ?
|
|
Braintree.Environment.PRODUCTION : Braintree.Environment.SANDBOX,
|
|
MerchantId = _globalSettings.Braintree.MerchantId,
|
|
PublicKey = _globalSettings.Braintree.PublicKey,
|
|
PrivateKey = _globalSettings.Braintree.PrivateKey
|
|
};
|
|
|
|
var btObjIdField = model.Id[0] == 'o' ? "organization_id" : "user_id";
|
|
var btObjId = new Guid(model.Id.Substring(1, 32));
|
|
|
|
var transactionResult = await btGateway.Transaction.SaleAsync(
|
|
new Braintree.TransactionRequest
|
|
{
|
|
Amount = model.Amount.Value,
|
|
CustomerId = model.Id,
|
|
Options = new Braintree.TransactionOptionsRequest
|
|
{
|
|
SubmitForSettlement = true,
|
|
PayPal = new Braintree.TransactionOptionsPayPalRequest
|
|
{
|
|
CustomField = $"{btObjIdField}:{btObjId}"
|
|
}
|
|
},
|
|
CustomFields = new Dictionary<string, string>
|
|
{
|
|
[btObjIdField] = btObjId.ToString()
|
|
}
|
|
});
|
|
|
|
if (!transactionResult.IsSuccess())
|
|
{
|
|
ModelState.AddModelError(string.Empty, "Charge failed. " +
|
|
"Refer to Braintree admin portal for more information.");
|
|
}
|
|
else
|
|
{
|
|
model.TransactionId = transactionResult.Target.Id;
|
|
model.PayPalTransactionId = transactionResult.Target?.PayPalDetails?.CaptureId;
|
|
}
|
|
return View(model);
|
|
}
|
|
|
|
public IActionResult CreateTransaction(Guid? organizationId = null, Guid? userId = null)
|
|
{
|
|
return View("CreateUpdateTransaction", new CreateUpdateTransactionModel
|
|
{
|
|
OrganizationId = organizationId,
|
|
UserId = userId
|
|
});
|
|
}
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> CreateTransaction(CreateUpdateTransactionModel model)
|
|
{
|
|
if (!ModelState.IsValid)
|
|
{
|
|
return View("CreateUpdateTransaction", model);
|
|
}
|
|
|
|
await _transactionRepository.CreateAsync(model.ToTransaction());
|
|
if (model.UserId.HasValue)
|
|
{
|
|
return RedirectToAction("Edit", "Users", new { id = model.UserId });
|
|
}
|
|
else
|
|
{
|
|
return RedirectToAction("Edit", "Organizations", new { id = model.OrganizationId });
|
|
}
|
|
}
|
|
|
|
public async Task<IActionResult> EditTransaction(Guid id)
|
|
{
|
|
var transaction = await _transactionRepository.GetByIdAsync(id);
|
|
if (transaction == null)
|
|
{
|
|
return RedirectToAction("Index", "Home");
|
|
}
|
|
return View("CreateUpdateTransaction", new CreateUpdateTransactionModel(transaction));
|
|
}
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> EditTransaction(Guid id, CreateUpdateTransactionModel model)
|
|
{
|
|
if (!ModelState.IsValid)
|
|
{
|
|
return View("CreateUpdateTransaction", model);
|
|
}
|
|
await _transactionRepository.ReplaceAsync(model.ToTransaction(id));
|
|
if (model.UserId.HasValue)
|
|
{
|
|
return RedirectToAction("Edit", "Users", new { id = model.UserId });
|
|
}
|
|
else
|
|
{
|
|
return RedirectToAction("Edit", "Organizations", new { id = model.OrganizationId });
|
|
}
|
|
}
|
|
|
|
public IActionResult PromoteAdmin()
|
|
{
|
|
return View();
|
|
}
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> PromoteAdmin(PromoteAdminModel model)
|
|
{
|
|
if (!ModelState.IsValid)
|
|
{
|
|
return View(model);
|
|
}
|
|
|
|
var orgUsers = await _organizationUserRepository.GetManyByOrganizationAsync(
|
|
model.OrganizationId.Value, null);
|
|
var user = orgUsers.FirstOrDefault(u => u.UserId == model.UserId.Value);
|
|
if (user == null)
|
|
{
|
|
ModelState.AddModelError(nameof(model.UserId), "User Id not found in this organization.");
|
|
}
|
|
else if (user.Type != Core.Enums.OrganizationUserType.Admin)
|
|
{
|
|
ModelState.AddModelError(nameof(model.UserId), "User is not an admin of this organization.");
|
|
}
|
|
|
|
if (!ModelState.IsValid)
|
|
{
|
|
return View(model);
|
|
}
|
|
|
|
user.Type = Core.Enums.OrganizationUserType.Owner;
|
|
await _organizationUserRepository.ReplaceAsync(user);
|
|
return RedirectToAction("Edit", "Organizations", new { id = model.OrganizationId.Value });
|
|
}
|
|
|
|
public IActionResult GenerateLicense()
|
|
{
|
|
return View();
|
|
}
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> GenerateLicense(LicenseModel model)
|
|
{
|
|
if (!ModelState.IsValid)
|
|
{
|
|
return View(model);
|
|
}
|
|
|
|
User user = null;
|
|
Organization organization = null;
|
|
if (model.UserId.HasValue)
|
|
{
|
|
user = await _userService.GetUserByIdAsync(model.UserId.Value);
|
|
if (user == null)
|
|
{
|
|
ModelState.AddModelError(nameof(model.UserId), "User Id not found.");
|
|
}
|
|
}
|
|
else if (model.OrganizationId.HasValue)
|
|
{
|
|
organization = await _organizationRepository.GetByIdAsync(model.OrganizationId.Value);
|
|
if (organization == null)
|
|
{
|
|
ModelState.AddModelError(nameof(model.OrganizationId), "Organization not found.");
|
|
}
|
|
else if (!organization.Enabled)
|
|
{
|
|
ModelState.AddModelError(nameof(model.OrganizationId), "Organization is disabled.");
|
|
}
|
|
}
|
|
if (model.InstallationId.HasValue)
|
|
{
|
|
var installation = await _installationRepository.GetByIdAsync(model.InstallationId.Value);
|
|
if (installation == null)
|
|
{
|
|
ModelState.AddModelError(nameof(model.InstallationId), "Installation not found.");
|
|
}
|
|
else if (!installation.Enabled)
|
|
{
|
|
ModelState.AddModelError(nameof(model.OrganizationId), "Installation is disabled.");
|
|
}
|
|
}
|
|
|
|
if (!ModelState.IsValid)
|
|
{
|
|
return View(model);
|
|
}
|
|
|
|
if (organization != null)
|
|
{
|
|
var license = await _organizationService.GenerateLicenseAsync(organization,
|
|
model.InstallationId.Value, model.Version);
|
|
return File(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(license, Formatting.Indented)),
|
|
"text/plain", "bitwarden_organization_license.json");
|
|
}
|
|
else if (user != null)
|
|
{
|
|
var license = await _userService.GenerateLicenseAsync(user, null, model.Version);
|
|
return File(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(license, Formatting.Indented)),
|
|
"text/plain", "bitwarden_premium_license.json");
|
|
}
|
|
else
|
|
{
|
|
throw new Exception("No license to generate.");
|
|
}
|
|
}
|
|
|
|
public async Task<IActionResult> TaxRate(int page = 1, int count = 25)
|
|
{
|
|
if (page < 1)
|
|
{
|
|
page = 1;
|
|
}
|
|
|
|
if (count < 1)
|
|
{
|
|
count = 1;
|
|
}
|
|
|
|
var skip = (page - 1) * count;
|
|
var rates = await _taxRateRepository.SearchAsync(skip, count);
|
|
return View(new TaxRatesModel
|
|
{
|
|
Items = rates.ToList(),
|
|
Page = page,
|
|
Count = count
|
|
});
|
|
}
|
|
|
|
public async Task<IActionResult> TaxRateAddEdit(string stripeTaxRateId = null)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(stripeTaxRateId))
|
|
{
|
|
return View(new TaxRateAddEditModel());
|
|
}
|
|
|
|
var rate = await _taxRateRepository.GetByIdAsync(stripeTaxRateId);
|
|
var model = new TaxRateAddEditModel()
|
|
{
|
|
StripeTaxRateId = stripeTaxRateId,
|
|
Country = rate.Country,
|
|
State = rate.State,
|
|
PostalCode = rate.PostalCode,
|
|
Rate = rate.Rate
|
|
};
|
|
|
|
return View(model);
|
|
}
|
|
|
|
public async Task<IActionResult> TaxRateUpload(IFormFile file)
|
|
{
|
|
if (file == null || file.Length == 0)
|
|
{
|
|
throw new ArgumentNullException(nameof(file));
|
|
}
|
|
|
|
// Build rates and validate them first before updating DB & Stripe
|
|
var taxRateUpdates = new List<TaxRate>();
|
|
var currentTaxRates = await _taxRateRepository.GetAllActiveAsync();
|
|
using var reader = new StreamReader(file.OpenReadStream());
|
|
while (!reader.EndOfStream)
|
|
{
|
|
var line = await reader.ReadLineAsync();
|
|
if (string.IsNullOrWhiteSpace(line))
|
|
{
|
|
continue;
|
|
}
|
|
var taxParts = line.Split(',');
|
|
if (taxParts.Length < 2)
|
|
{
|
|
throw new Exception($"This line is not in the format of <postal code>,<rate>,<state code>,<country code>: {line}");
|
|
}
|
|
var postalCode = taxParts[0].Trim();
|
|
if (string.IsNullOrWhiteSpace(postalCode))
|
|
{
|
|
throw new Exception($"'{line}' is not valid, the first element must contain a postal code.");
|
|
}
|
|
if (!decimal.TryParse(taxParts[1], out var rate) || rate <= 0M || rate > 100)
|
|
{
|
|
throw new Exception($"{taxParts[1]} is not a valid rate/decimal for {postalCode}");
|
|
}
|
|
var state = taxParts.Length > 2 ? taxParts[2] : null;
|
|
var country = (taxParts.Length > 3 ? taxParts[3] : null);
|
|
if (string.IsNullOrWhiteSpace(country))
|
|
{
|
|
country = "US";
|
|
}
|
|
var taxRate = currentTaxRates.FirstOrDefault(r => r.Country == country && r.PostalCode == postalCode) ??
|
|
new TaxRate
|
|
{
|
|
Country = country,
|
|
PostalCode = postalCode,
|
|
Active = true,
|
|
};
|
|
taxRate.Rate = rate;
|
|
taxRate.State = state ?? taxRate.State;
|
|
taxRateUpdates.Add(taxRate);
|
|
}
|
|
|
|
foreach (var taxRate in taxRateUpdates)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(taxRate.Id))
|
|
{
|
|
await _paymentService.UpdateTaxRateAsync(taxRate);
|
|
}
|
|
else
|
|
{
|
|
await _paymentService.CreateTaxRateAsync(taxRate);
|
|
}
|
|
}
|
|
|
|
return RedirectToAction("TaxRate");
|
|
}
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> TaxRateAddEdit(TaxRateAddEditModel model)
|
|
{
|
|
var existingRateCheck = await _taxRateRepository.GetByLocationAsync(new TaxRate() { Country = model.Country, PostalCode = model.PostalCode });
|
|
if (existingRateCheck.Any())
|
|
{
|
|
ModelState.AddModelError(nameof(model.PostalCode), "A tax rate already exists for this Country/Postal Code combination.");
|
|
}
|
|
|
|
if (!ModelState.IsValid)
|
|
{
|
|
return View(model);
|
|
}
|
|
|
|
var taxRate = new TaxRate()
|
|
{
|
|
Id = model.StripeTaxRateId,
|
|
Country = model.Country,
|
|
State = model.State,
|
|
PostalCode = model.PostalCode,
|
|
Rate = model.Rate
|
|
};
|
|
|
|
if (!string.IsNullOrWhiteSpace(model.StripeTaxRateId))
|
|
{
|
|
await _paymentService.UpdateTaxRateAsync(taxRate);
|
|
}
|
|
else
|
|
{
|
|
await _paymentService.CreateTaxRateAsync(taxRate);
|
|
}
|
|
|
|
return RedirectToAction("TaxRate");
|
|
}
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> TaxRateUpdate(TaxRate model)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(model.Id))
|
|
{
|
|
await _paymentService.UpdateTaxRateAsync(model);
|
|
}
|
|
|
|
return RedirectToAction("TaxRate");
|
|
}
|
|
|
|
public async Task<IActionResult> TaxRateArchive(string stripeTaxRateId)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(stripeTaxRateId))
|
|
{
|
|
await _paymentService.ArchiveTaxRateAsync(new TaxRate() { Id = stripeTaxRateId });
|
|
}
|
|
|
|
return RedirectToAction("TaxRate");
|
|
}
|
|
}
|
|
}
|