mirror of
https://github.com/bitwarden/server.git
synced 2025-06-09 12:40:32 -05:00
Merge branch 'main' into test-docker-stuff
This commit is contained in:
commit
def622f77d
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@ -37,6 +37,8 @@ util/Setup/** @bitwarden/dept-bre @bitwarden/team-platform-dev
|
||||
**/Auth @bitwarden/team-auth-dev
|
||||
bitwarden_license/src/Sso @bitwarden/team-auth-dev
|
||||
src/Identity @bitwarden/team-auth-dev
|
||||
src/Core/Identity @bitwarden/team-auth-dev
|
||||
src/Core/IdentityServer @bitwarden/team-auth-dev
|
||||
|
||||
# Key Management team
|
||||
**/KeyManagement @bitwarden/team-key-management-dev
|
||||
|
13
.github/renovate.json5
vendored
13
.github/renovate.json5
vendored
@ -9,6 +9,19 @@
|
||||
"nuget",
|
||||
],
|
||||
packageRules: [
|
||||
{
|
||||
// Group all release-related workflows for GitHub Actions together for BRE.
|
||||
groupName: "github-action",
|
||||
matchManagers: ["github-actions"],
|
||||
matchFileNames: [
|
||||
".github/workflows/publish.yml",
|
||||
".github/workflows/release.yml",
|
||||
".github/workflows/repository-management.yml"
|
||||
],
|
||||
commitMessagePrefix: "[deps] BRE:",
|
||||
reviewers: ["team:dept-bre"],
|
||||
addLabels: ["hold"]
|
||||
},
|
||||
{
|
||||
groupName: "dockerfile minor",
|
||||
matchManagers: ["dockerfile"],
|
||||
|
@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<Version>2025.4.1</Version>
|
||||
<Version>2025.4.3</Version>
|
||||
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
@ -8,7 +8,7 @@ using Bit.Core.Utilities;
|
||||
using Bit.Scim.Context;
|
||||
using Bit.Scim.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Duende.IdentityModel;
|
||||
using IdentityModel;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Stripe;
|
||||
|
||||
|
@ -3,7 +3,7 @@ using System.Text.Encodings.Web;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Scim.Context;
|
||||
using Duende.IdentityModel;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
@ -19,10 +19,10 @@ using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Sso.Models;
|
||||
using Bit.Sso.Utilities;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -7,9 +7,9 @@ using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Sso.Models;
|
||||
using Bit.Sso.Utilities;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Infrastructure;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
0
dev/ef_migrate.ps1
Normal file → Executable file
0
dev/ef_migrate.ps1
Normal file → Executable file
@ -25,6 +25,12 @@
|
||||
"Subscriptions": [
|
||||
{
|
||||
"Name": "events-write-subscription"
|
||||
},
|
||||
{
|
||||
"Name": "events-slack-subscription"
|
||||
},
|
||||
{
|
||||
"Name": "events-webhook-subscription"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -462,6 +462,7 @@ public class OrganizationsController : Controller
|
||||
organization.UsersGetPremium = model.UsersGetPremium;
|
||||
organization.UseSecretsManager = model.UseSecretsManager;
|
||||
organization.UseRiskInsights = model.UseRiskInsights;
|
||||
organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies;
|
||||
|
||||
//secrets
|
||||
organization.SmSeats = model.SmSeats;
|
||||
|
@ -3,11 +3,13 @@ using System.Net;
|
||||
using Bit.Admin.AdminConsole.Models;
|
||||
using Bit.Admin.Enums;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
@ -23,6 +25,7 @@ using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Controllers;
|
||||
|
||||
@ -44,6 +47,7 @@ public class ProvidersController : Controller
|
||||
private readonly IProviderPlanRepository _providerPlanRepository;
|
||||
private readonly IProviderBillingService _providerBillingService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IStripeAdapter _stripeAdapter;
|
||||
private readonly string _stripeUrl;
|
||||
private readonly string _braintreeMerchantUrl;
|
||||
private readonly string _braintreeMerchantId;
|
||||
@ -63,7 +67,8 @@ public class ProvidersController : Controller
|
||||
IProviderPlanRepository providerPlanRepository,
|
||||
IProviderBillingService providerBillingService,
|
||||
IWebHostEnvironment webHostEnvironment,
|
||||
IPricingClient pricingClient)
|
||||
IPricingClient pricingClient,
|
||||
IStripeAdapter stripeAdapter)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationService = organizationService;
|
||||
@ -79,6 +84,7 @@ public class ProvidersController : Controller
|
||||
_providerPlanRepository = providerPlanRepository;
|
||||
_providerBillingService = providerBillingService;
|
||||
_pricingClient = pricingClient;
|
||||
_stripeAdapter = stripeAdapter;
|
||||
_stripeUrl = webHostEnvironment.GetStripeUrl();
|
||||
_braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl();
|
||||
_braintreeMerchantId = globalSettings.Braintree.MerchantId;
|
||||
@ -306,6 +312,23 @@ public class ProvidersController : Controller
|
||||
(Plan: PlanType.EnterpriseMonthly, SeatsMinimum: model.EnterpriseMonthlySeatMinimum)
|
||||
]);
|
||||
await _providerBillingService.UpdateSeatMinimums(updateMspSeatMinimumsCommand);
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically))
|
||||
{
|
||||
var customer = await _stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId);
|
||||
|
||||
if (model.PayByInvoice != customer.ApprovedToPayByInvoice())
|
||||
{
|
||||
var approvedToPayByInvoice = model.PayByInvoice ? "1" : "0";
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||
{
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
[StripeConstants.MetadataKeys.InvoiceApproved] = approvedToPayByInvoice
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ProviderType.BusinessUnit:
|
||||
{
|
||||
@ -345,14 +368,18 @@ public class ProvidersController : Controller
|
||||
|
||||
if (!provider.IsBillable())
|
||||
{
|
||||
return new ProviderEditModel(provider, users, providerOrganizations, new List<ProviderPlan>());
|
||||
return new ProviderEditModel(provider, users, providerOrganizations, new List<ProviderPlan>(), false);
|
||||
}
|
||||
|
||||
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
||||
|
||||
var payByInvoice =
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically) &&
|
||||
(await _stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId)).ApprovedToPayByInvoice();
|
||||
|
||||
return new ProviderEditModel(
|
||||
provider, users, providerOrganizations,
|
||||
providerPlans.ToList(), GetGatewayCustomerUrl(provider), GetGatewaySubscriptionUrl(provider));
|
||||
providerPlans.ToList(), payByInvoice, GetGatewayCustomerUrl(provider), GetGatewaySubscriptionUrl(provider));
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Provider_ResendEmailInvite)]
|
||||
|
@ -86,6 +86,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
UseApi = org.UseApi;
|
||||
UseSecretsManager = org.UseSecretsManager;
|
||||
UseRiskInsights = org.UseRiskInsights;
|
||||
UseAdminSponsoredFamilies = org.UseAdminSponsoredFamilies;
|
||||
UseResetPassword = org.UseResetPassword;
|
||||
SelfHost = org.SelfHost;
|
||||
UsersGetPremium = org.UsersGetPremium;
|
||||
@ -154,6 +155,8 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
public new bool UseSecretsManager { get; set; }
|
||||
[Display(Name = "Risk Insights")]
|
||||
public new bool UseRiskInsights { get; set; }
|
||||
[Display(Name = "Admin Sponsored Families")]
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
[Display(Name = "Self Host")]
|
||||
public bool SelfHost { get; set; }
|
||||
[Display(Name = "Users Get Premium")]
|
||||
@ -295,6 +298,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
existingOrganization.UseApi = UseApi;
|
||||
existingOrganization.UseSecretsManager = UseSecretsManager;
|
||||
existingOrganization.UseRiskInsights = UseRiskInsights;
|
||||
existingOrganization.UseAdminSponsoredFamilies = UseAdminSponsoredFamilies;
|
||||
existingOrganization.UseResetPassword = UseResetPassword;
|
||||
existingOrganization.SelfHost = SelfHost;
|
||||
existingOrganization.UsersGetPremium = UsersGetPremium;
|
||||
|
@ -18,6 +18,7 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
||||
IEnumerable<ProviderUserUserDetails> providerUsers,
|
||||
IEnumerable<ProviderOrganizationOrganizationDetails> organizations,
|
||||
IReadOnlyCollection<ProviderPlan> providerPlans,
|
||||
bool payByInvoice,
|
||||
string gatewayCustomerUrl = null,
|
||||
string gatewaySubscriptionUrl = null) : base(provider, providerUsers, organizations, providerPlans)
|
||||
{
|
||||
@ -33,6 +34,7 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
||||
GatewayCustomerUrl = gatewayCustomerUrl;
|
||||
GatewaySubscriptionUrl = gatewaySubscriptionUrl;
|
||||
Type = provider.Type;
|
||||
PayByInvoice = payByInvoice;
|
||||
|
||||
if (Type == ProviderType.BusinessUnit)
|
||||
{
|
||||
@ -62,6 +64,8 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
||||
public string GatewaySubscriptionId { get; set; }
|
||||
public string GatewayCustomerUrl { get; }
|
||||
public string GatewaySubscriptionUrl { get; }
|
||||
[Display(Name = "Pay By Invoice")]
|
||||
public bool PayByInvoice { get; set; }
|
||||
[Display(Name = "Provider Type")]
|
||||
public ProviderType Type { get; set; }
|
||||
|
||||
|
@ -136,6 +136,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically) && Model.Provider.Type == ProviderType.Msp && Model.Provider.IsBillable())
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" class="form-check-input" asp-for="PayByInvoice">
|
||||
<label class="form-check-label" asp-for="PayByInvoice"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</form>
|
||||
@await Html.PartialAsync("Organizations", Model)
|
||||
|
@ -1,9 +1,11 @@
|
||||
@using Bit.Admin.Enums;
|
||||
@using Bit.Core
|
||||
@using Bit.Core.Enums
|
||||
@using Bit.Core.AdminConsole.Enums.Provider
|
||||
@using Bit.Core.Billing.Enums
|
||||
@using Bit.SharedWeb.Utilities
|
||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService;
|
||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||
|
||||
@model OrganizationEditModel
|
||||
|
||||
@ -146,6 +148,13 @@
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseCustomPermissions" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UseCustomPermissions"></label>
|
||||
</div>
|
||||
@if(FeatureService.IsEnabled(FeatureFlagKeys.PM17772_AdminInitiatedSponsorships))
|
||||
{
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseAdminSponsoredFamilies" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UseAdminSponsoredFamilies"></label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<h3>Password Manager</h3>
|
||||
|
21
src/Api/AdminConsole/Authorization/AuthorizeAttribute.cs
Normal file
21
src/Api/AdminConsole/Authorization/AuthorizeAttribute.cs
Normal file
@ -0,0 +1,21 @@
|
||||
#nullable enable
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// An attribute which requires authorization using the specified requirement.
|
||||
/// This uses the standard ASP.NET authorization middleware.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The IAuthorizationRequirement that will be used to authorize the user.</typeparam>
|
||||
public class AuthorizeAttribute<T>
|
||||
: AuthorizeAttribute, IAuthorizationRequirementData
|
||||
where T : IAuthorizationRequirement, new()
|
||||
{
|
||||
public IEnumerable<IAuthorizationRequirement> GetRequirements()
|
||||
{
|
||||
var requirement = new T();
|
||||
return [requirement];
|
||||
}
|
||||
}
|
79
src/Api/AdminConsole/Authorization/HttpContextExtensions.cs
Normal file
79
src/Api/AdminConsole/Authorization/HttpContextExtensions.cs
Normal file
@ -0,0 +1,79 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Authorization;
|
||||
|
||||
public static class HttpContextExtensions
|
||||
{
|
||||
public const string NoOrgIdError =
|
||||
"A route decorated with with '[Authorize<Requirement>]' must include a route value named 'orgId' either through the [Controller] attribute or through a '[Http*]' attribute.";
|
||||
|
||||
/// <summary>
|
||||
/// Returns the result of the callback, caching it in HttpContext.Features for the lifetime of the request.
|
||||
/// Subsequent calls will retrieve the cached value.
|
||||
/// Results are stored by type and therefore must be of a unique type.
|
||||
/// </summary>
|
||||
public static async Task<T> WithFeaturesCacheAsync<T>(this HttpContext httpContext, Func<Task<T>> callback)
|
||||
{
|
||||
var cachedResult = httpContext.Features.Get<T>();
|
||||
if (cachedResult != null)
|
||||
{
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
var result = await callback();
|
||||
httpContext.Features.Set(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the user is a ProviderUser for a Provider which manages the specified organization, otherwise false.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This data is fetched from the database and cached as a HttpContext Feature for the lifetime of the request.
|
||||
/// </remarks>
|
||||
public static async Task<bool> IsProviderUserForOrgAsync(
|
||||
this HttpContext httpContext,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
Guid userId,
|
||||
Guid organizationId)
|
||||
{
|
||||
var organizations = await httpContext.GetProviderUserOrganizationsAsync(providerUserRepository, userId);
|
||||
return organizations.Any(o => o.OrganizationId == organizationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the ProviderUserOrganizations for a user. These are the organizations the ProviderUser manages via their Provider, if any.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This data is fetched from the database and cached as a HttpContext Feature for the lifetime of the request.
|
||||
/// </remarks>
|
||||
private static async Task<IEnumerable<ProviderUserOrganizationDetails>> GetProviderUserOrganizationsAsync(
|
||||
this HttpContext httpContext,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
Guid userId)
|
||||
=> await httpContext.WithFeaturesCacheAsync(() =>
|
||||
providerUserRepository.GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed));
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Parses the {orgId} route parameter into a Guid, or throws if the {orgId} is not present or not a valid guid.
|
||||
/// </summary>
|
||||
/// <param name="httpContext"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public static Guid GetOrganizationId(this HttpContext httpContext)
|
||||
{
|
||||
httpContext.GetRouteData().Values.TryGetValue("orgId", out var orgIdParam);
|
||||
if (orgIdParam == null || !Guid.TryParse(orgIdParam.ToString(), out var orgId))
|
||||
{
|
||||
throw new InvalidOperationException(NoOrgIdError);
|
||||
}
|
||||
|
||||
return orgId;
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.Context;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// A requirement that implements this interface will be handled by <see cref="OrganizationRequirementHandler"/>,
|
||||
/// which calls AuthorizeAsync with the organization details from the route.
|
||||
/// This is used for simple role-based checks.
|
||||
/// This may only be used on endpoints with {orgId} in their path.
|
||||
/// </summary>
|
||||
public interface IOrganizationRequirement : IAuthorizationRequirement
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to authorize a request that has this requirement.
|
||||
/// </summary>
|
||||
/// <param name="organizationClaims">
|
||||
/// The CurrentContextOrganization for the user if they are a member of the organization.
|
||||
/// This is null if they are not a member.
|
||||
/// </param>
|
||||
/// <param name="isProviderUserForOrg">
|
||||
/// A callback that returns true if the user is a ProviderUser that manages the organization, otherwise false.
|
||||
/// This requires a database query, call it last.
|
||||
/// </param>
|
||||
/// <returns>True if the requirement has been satisfied, otherwise false.</returns>
|
||||
public Task<bool> AuthorizeAsync(
|
||||
CurrentContextOrganization? organizationClaims,
|
||||
Func<Task<bool>> isProviderUserForOrg);
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Models.Data;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Authorization;
|
||||
|
||||
public static class OrganizationClaimsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a user's claims and returns an object representing their claims for the specified organization.
|
||||
/// </summary>
|
||||
/// <param name="user">The user who has the claims.</param>
|
||||
/// <param name="organizationId">The organizationId to look for in the claims.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="CurrentContextOrganization"/> representing the user's claims for that organization, or null
|
||||
/// if the user does not have any claims for that organization.
|
||||
/// </returns>
|
||||
public static CurrentContextOrganization? GetCurrentContextOrganization(this ClaimsPrincipal user, Guid organizationId)
|
||||
{
|
||||
var hasClaim = GetClaimsParser(user, organizationId);
|
||||
|
||||
var role = GetRoleFromClaims(hasClaim);
|
||||
if (!role.HasValue)
|
||||
{
|
||||
// Not an organization member
|
||||
return null;
|
||||
}
|
||||
|
||||
return new CurrentContextOrganization
|
||||
{
|
||||
Id = organizationId,
|
||||
Type = role.Value,
|
||||
AccessSecretsManager = hasClaim(Claims.SecretsManagerAccess),
|
||||
Permissions = role == OrganizationUserType.Custom
|
||||
? GetPermissionsFromClaims(hasClaim)
|
||||
: new Permissions()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a function for evaluating claims for the specified user and organizationId.
|
||||
/// The function returns true if the claim type exists and false otherwise.
|
||||
/// </summary>
|
||||
private static Func<string, bool> GetClaimsParser(ClaimsPrincipal user, Guid organizationId)
|
||||
{
|
||||
// Group claims by ClaimType
|
||||
var claimsDict = user.Claims
|
||||
.GroupBy(c => c.Type)
|
||||
.ToDictionary(
|
||||
c => c.Key,
|
||||
c => c.ToList());
|
||||
|
||||
return claimType
|
||||
=> claimsDict.TryGetValue(claimType, out var claims) &&
|
||||
claims
|
||||
.ParseGuids()
|
||||
.Any(v => v == organizationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the provided claims into proper Guids, or ignore them if they are not valid guids.
|
||||
/// </summary>
|
||||
private static IEnumerable<Guid> ParseGuids(this IEnumerable<Claim> claims)
|
||||
{
|
||||
foreach (var claim in claims)
|
||||
{
|
||||
if (Guid.TryParse(claim.Value, out var guid))
|
||||
{
|
||||
yield return guid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static OrganizationUserType? GetRoleFromClaims(Func<string, bool> hasClaim)
|
||||
{
|
||||
if (hasClaim(Claims.OrganizationOwner))
|
||||
{
|
||||
return OrganizationUserType.Owner;
|
||||
}
|
||||
|
||||
if (hasClaim(Claims.OrganizationAdmin))
|
||||
{
|
||||
return OrganizationUserType.Admin;
|
||||
}
|
||||
|
||||
if (hasClaim(Claims.OrganizationCustom))
|
||||
{
|
||||
return OrganizationUserType.Custom;
|
||||
}
|
||||
|
||||
if (hasClaim(Claims.OrganizationUser))
|
||||
{
|
||||
return OrganizationUserType.User;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Permissions GetPermissionsFromClaims(Func<string, bool> hasClaim)
|
||||
=> new()
|
||||
{
|
||||
AccessEventLogs = hasClaim(Claims.CustomPermissions.AccessEventLogs),
|
||||
AccessImportExport = hasClaim(Claims.CustomPermissions.AccessImportExport),
|
||||
AccessReports = hasClaim(Claims.CustomPermissions.AccessReports),
|
||||
CreateNewCollections = hasClaim(Claims.CustomPermissions.CreateNewCollections),
|
||||
EditAnyCollection = hasClaim(Claims.CustomPermissions.EditAnyCollection),
|
||||
DeleteAnyCollection = hasClaim(Claims.CustomPermissions.DeleteAnyCollection),
|
||||
ManageGroups = hasClaim(Claims.CustomPermissions.ManageGroups),
|
||||
ManagePolicies = hasClaim(Claims.CustomPermissions.ManagePolicies),
|
||||
ManageSso = hasClaim(Claims.CustomPermissions.ManageSso),
|
||||
ManageUsers = hasClaim(Claims.CustomPermissions.ManageUsers),
|
||||
ManageResetPassword = hasClaim(Claims.CustomPermissions.ManageResetPassword),
|
||||
ManageScim = hasClaim(Claims.CustomPermissions.ManageScim),
|
||||
};
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// Handles any requirement that implements <see cref="IOrganizationRequirement"/>.
|
||||
/// Retrieves the Organization ID from the route and then passes it to the requirement's AuthorizeAsync callback to
|
||||
/// determine whether the action is authorized.
|
||||
/// </summary>
|
||||
public class OrganizationRequirementHandler(
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IUserService userService)
|
||||
: AuthorizationHandler<IOrganizationRequirement>
|
||||
{
|
||||
public const string NoHttpContextError = "This method should only be called in the context of an HTTP Request.";
|
||||
public const string NoUserIdError = "This method should only be called on the private api with a logged in user.";
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, IOrganizationRequirement requirement)
|
||||
{
|
||||
var httpContext = httpContextAccessor.HttpContext;
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new InvalidOperationException(NoHttpContextError);
|
||||
}
|
||||
|
||||
var organizationId = httpContext.GetOrganizationId();
|
||||
var organizationClaims = httpContext.User.GetCurrentContextOrganization(organizationId);
|
||||
|
||||
var userId = userService.GetProperUserId(httpContext.User);
|
||||
if (userId == null)
|
||||
{
|
||||
throw new InvalidOperationException(NoUserIdError);
|
||||
}
|
||||
|
||||
Task<bool> IsProviderUserForOrg() => httpContext.IsProviderUserForOrgAsync(providerUserRepository, userId.Value, organizationId);
|
||||
|
||||
var authorized = await requirement.AuthorizeAsync(organizationClaims, IsProviderUserForOrg);
|
||||
|
||||
if (authorized)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Authorization.Requirements;
|
||||
|
||||
public class ManageUsersRequirement : IOrganizationRequirement
|
||||
{
|
||||
public async Task<bool> AuthorizeAsync(
|
||||
CurrentContextOrganization? organizationClaims,
|
||||
Func<Task<bool>> isProviderUserForOrg)
|
||||
=> organizationClaims switch
|
||||
{
|
||||
{ Type: OrganizationUserType.Owner } => true,
|
||||
{ Type: OrganizationUserType.Admin } => true,
|
||||
{ Permissions.ManageUsers: true } => true,
|
||||
_ => await isProviderUserForOrg()
|
||||
};
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.Context;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Authorization.Requirements;
|
||||
|
||||
/// <summary>
|
||||
/// Requires that the user is a member of the organization or a provider for the organization.
|
||||
/// </summary>
|
||||
public class MemberOrProviderRequirement : IOrganizationRequirement
|
||||
{
|
||||
public async Task<bool> AuthorizeAsync(
|
||||
CurrentContextOrganization? organizationClaims,
|
||||
Func<Task<bool>> isProviderUserForOrg)
|
||||
=> organizationClaims is not null || await isProviderUserForOrg();
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Controllers;
|
||||
|
||||
[Route("organizations/{organizationId:guid}/integrations/{integrationId:guid}/configurations")]
|
||||
[Authorize("Application")]
|
||||
public class OrganizationIntegrationConfigurationController(
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationIntegrationRepository integrationRepository,
|
||||
IOrganizationIntegrationConfigurationRepository integrationConfigurationRepository) : Controller
|
||||
{
|
||||
[HttpPost("")]
|
||||
public async Task<OrganizationIntegrationConfigurationResponseModel> CreateAsync(
|
||||
Guid organizationId,
|
||||
Guid integrationId,
|
||||
[FromBody] OrganizationIntegrationConfigurationRequestModel model)
|
||||
{
|
||||
if (!await HasPermission(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
||||
if (integration == null || integration.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
if (!model.IsValidForType(integration.Type))
|
||||
{
|
||||
throw new BadRequestException($"Invalid Configuration and/or Template for integration type {integration.Type}");
|
||||
}
|
||||
|
||||
var organizationIntegrationConfiguration = model.ToOrganizationIntegrationConfiguration(integrationId);
|
||||
var configuration = await integrationConfigurationRepository.CreateAsync(organizationIntegrationConfiguration);
|
||||
return new OrganizationIntegrationConfigurationResponseModel(configuration);
|
||||
}
|
||||
|
||||
[HttpPut("{configurationId:guid}")]
|
||||
public async Task<OrganizationIntegrationConfigurationResponseModel> UpdateAsync(
|
||||
Guid organizationId,
|
||||
Guid integrationId,
|
||||
Guid configurationId,
|
||||
[FromBody] OrganizationIntegrationConfigurationRequestModel model)
|
||||
{
|
||||
if (!await HasPermission(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
||||
if (integration == null || integration.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
if (!model.IsValidForType(integration.Type))
|
||||
{
|
||||
throw new BadRequestException($"Invalid Configuration and/or Template for integration type {integration.Type}");
|
||||
}
|
||||
|
||||
var configuration = await integrationConfigurationRepository.GetByIdAsync(configurationId);
|
||||
if (configuration is null || configuration.OrganizationIntegrationId != integrationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var newConfiguration = model.ToOrganizationIntegrationConfiguration(configuration);
|
||||
await integrationConfigurationRepository.ReplaceAsync(newConfiguration);
|
||||
|
||||
return new OrganizationIntegrationConfigurationResponseModel(newConfiguration);
|
||||
}
|
||||
|
||||
[HttpDelete("{configurationId:guid}")]
|
||||
[HttpPost("{configurationId:guid}/delete")]
|
||||
public async Task DeleteAsync(Guid organizationId, Guid integrationId, Guid configurationId)
|
||||
{
|
||||
if (!await HasPermission(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
||||
if (integration == null || integration.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var configuration = await integrationConfigurationRepository.GetByIdAsync(configurationId);
|
||||
if (configuration is null || configuration.OrganizationIntegrationId != integrationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await integrationConfigurationRepository.DeleteAsync(configuration);
|
||||
}
|
||||
|
||||
private async Task<bool> HasPermission(Guid organizationId)
|
||||
{
|
||||
return await currentContext.OrganizationOwner(organizationId);
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Api.AdminConsole.Controllers;
|
||||
|
||||
[Route("organizations/{organizationId:guid}/integrations")]
|
||||
[Authorize("Application")]
|
||||
public class OrganizationIntegrationController(
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationIntegrationRepository integrationRepository) : Controller
|
||||
{
|
||||
[HttpPost("")]
|
||||
public async Task<OrganizationIntegrationResponseModel> CreateAsync(Guid organizationId, [FromBody] OrganizationIntegrationRequestModel model)
|
||||
{
|
||||
if (!await HasPermission(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var integration = await integrationRepository.CreateAsync(model.ToOrganizationIntegration(organizationId));
|
||||
return new OrganizationIntegrationResponseModel(integration);
|
||||
}
|
||||
|
||||
[HttpPut("{integrationId:guid}")]
|
||||
public async Task<OrganizationIntegrationResponseModel> UpdateAsync(Guid organizationId, Guid integrationId, [FromBody] OrganizationIntegrationRequestModel model)
|
||||
{
|
||||
if (!await HasPermission(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
||||
if (integration is null || integration.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await integrationRepository.ReplaceAsync(model.ToOrganizationIntegration(integration));
|
||||
return new OrganizationIntegrationResponseModel(integration);
|
||||
}
|
||||
|
||||
[HttpDelete("{integrationId:guid}")]
|
||||
[HttpPost("{integrationId:guid}/delete")]
|
||||
public async Task DeleteAsync(Guid organizationId, Guid integrationId)
|
||||
{
|
||||
if (!await HasPermission(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
||||
if (integration is null || integration.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await integrationRepository.DeleteAsync(integration);
|
||||
}
|
||||
|
||||
private async Task<bool> HasPermission(Guid organizationId)
|
||||
{
|
||||
return await currentContext.OrganizationOwner(organizationId);
|
||||
}
|
||||
}
|
@ -313,7 +313,7 @@ public class OrganizationUsersController : Controller
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
await _organizationService.InitPendingOrganization(user.Id, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName);
|
||||
await _organizationService.InitPendingOrganization(user, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName, model.Token);
|
||||
await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService);
|
||||
await _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id);
|
||||
}
|
||||
@ -494,7 +494,7 @@ public class OrganizationUsersController : Controller
|
||||
}
|
||||
|
||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgId);
|
||||
var isTdeEnrollment = ssoConfig != null && ssoConfig.GetData().MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption;
|
||||
var isTdeEnrollment = ssoConfig != null && ssoConfig.Enabled && ssoConfig.GetData().MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption;
|
||||
if (!isTdeEnrollment && !string.IsNullOrWhiteSpace(model.ResetPasswordKey) && !await _userService.VerifySecretAsync(user, model.MasterPasswordHash))
|
||||
{
|
||||
throw new BadRequestException("Incorrect password");
|
||||
|
@ -0,0 +1,77 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Integrations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Controllers;
|
||||
|
||||
[Route("organizations/{organizationId:guid}/integrations/slack")]
|
||||
[Authorize("Application")]
|
||||
public class SlackIntegrationController(
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationIntegrationRepository integrationRepository,
|
||||
ISlackService slackService) : Controller
|
||||
{
|
||||
[HttpGet("redirect")]
|
||||
public async Task<IActionResult> RedirectAsync(Guid organizationId)
|
||||
{
|
||||
if (!await currentContext.OrganizationOwner(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
string callbackUrl = Url.RouteUrl(
|
||||
nameof(CreateAsync),
|
||||
new { organizationId },
|
||||
currentContext.HttpContext.Request.Scheme);
|
||||
var redirectUrl = slackService.GetRedirectUrl(callbackUrl);
|
||||
|
||||
if (string.IsNullOrEmpty(redirectUrl))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return Redirect(redirectUrl);
|
||||
}
|
||||
|
||||
[HttpGet("create", Name = nameof(CreateAsync))]
|
||||
public async Task<IActionResult> CreateAsync(Guid organizationId, [FromQuery] string code)
|
||||
{
|
||||
if (!await currentContext.OrganizationOwner(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(code))
|
||||
{
|
||||
throw new BadRequestException("Missing code from Slack.");
|
||||
}
|
||||
|
||||
string callbackUrl = Url.RouteUrl(
|
||||
nameof(CreateAsync),
|
||||
new { organizationId },
|
||||
currentContext.HttpContext.Request.Scheme);
|
||||
var token = await slackService.ObtainTokenViaOAuth(code, callbackUrl);
|
||||
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
throw new BadRequestException("Invalid response from Slack.");
|
||||
}
|
||||
|
||||
var integration = await integrationRepository.CreateAsync(new OrganizationIntegration
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Type = IntegrationType.Slack,
|
||||
Configuration = JsonSerializer.Serialize(new SlackIntegration(token)),
|
||||
});
|
||||
var location = $"/organizations/{organizationId}/integrations/{integration.Id}";
|
||||
|
||||
return Created(location, new OrganizationIntegrationResponseModel(integration));
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Integrations;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
|
||||
public class OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
public string? Configuration { get; set; }
|
||||
|
||||
[Required]
|
||||
public EventType EventType { get; set; }
|
||||
|
||||
public string? Template { get; set; }
|
||||
|
||||
public bool IsValidForType(IntegrationType integrationType)
|
||||
{
|
||||
switch (integrationType)
|
||||
{
|
||||
case IntegrationType.CloudBillingSync or IntegrationType.Scim:
|
||||
return false;
|
||||
case IntegrationType.Slack:
|
||||
return !string.IsNullOrWhiteSpace(Template) && IsConfigurationValid<SlackIntegrationConfiguration>();
|
||||
case IntegrationType.Webhook:
|
||||
return !string.IsNullOrWhiteSpace(Template) && IsConfigurationValid<WebhookIntegrationConfiguration>();
|
||||
default:
|
||||
return false;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public OrganizationIntegrationConfiguration ToOrganizationIntegrationConfiguration(Guid organizationIntegrationId)
|
||||
{
|
||||
return new OrganizationIntegrationConfiguration()
|
||||
{
|
||||
OrganizationIntegrationId = organizationIntegrationId,
|
||||
Configuration = Configuration,
|
||||
EventType = EventType,
|
||||
Template = Template
|
||||
};
|
||||
}
|
||||
|
||||
public OrganizationIntegrationConfiguration ToOrganizationIntegrationConfiguration(OrganizationIntegrationConfiguration currentConfiguration)
|
||||
{
|
||||
currentConfiguration.Configuration = Configuration;
|
||||
currentConfiguration.EventType = EventType;
|
||||
currentConfiguration.Template = Template;
|
||||
|
||||
return currentConfiguration;
|
||||
}
|
||||
|
||||
private bool IsConfigurationValid<T>()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Configuration))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = JsonSerializer.Deserialize<T>(Configuration);
|
||||
return config is not null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
|
||||
public class OrganizationIntegrationRequestModel : IValidatableObject
|
||||
{
|
||||
public string? Configuration { get; set; }
|
||||
|
||||
public IntegrationType Type { get; set; }
|
||||
|
||||
public OrganizationIntegration ToOrganizationIntegration(Guid organizationId)
|
||||
{
|
||||
return new OrganizationIntegration()
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Configuration = Configuration,
|
||||
Type = Type,
|
||||
};
|
||||
}
|
||||
|
||||
public OrganizationIntegration ToOrganizationIntegration(OrganizationIntegration currentIntegration)
|
||||
{
|
||||
currentIntegration.Configuration = Configuration;
|
||||
return currentIntegration;
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case IntegrationType.CloudBillingSync or IntegrationType.Scim:
|
||||
yield return new ValidationResult($"{nameof(Type)} integrations are not yet supported.", new[] { nameof(Type) });
|
||||
break;
|
||||
case IntegrationType.Slack:
|
||||
yield return new ValidationResult($"{nameof(Type)} integrations cannot be created directly.", new[] { nameof(Type) });
|
||||
break;
|
||||
case IntegrationType.Webhook:
|
||||
if (Configuration is not null)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
"Webhook integrations must not include configuration.",
|
||||
new[] { nameof(Configuration) });
|
||||
}
|
||||
break;
|
||||
default:
|
||||
yield return new ValidationResult(
|
||||
$"Integration type '{Type}' is not recognized.",
|
||||
new[] { nameof(Type) });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
|
||||
public class OrganizationIntegrationConfigurationResponseModel : ResponseModel
|
||||
{
|
||||
public OrganizationIntegrationConfigurationResponseModel(OrganizationIntegrationConfiguration organizationIntegrationConfiguration, string obj = "organizationIntegrationConfiguration")
|
||||
: base(obj)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(organizationIntegrationConfiguration);
|
||||
|
||||
Id = organizationIntegrationConfiguration.Id;
|
||||
Configuration = organizationIntegrationConfiguration.Configuration;
|
||||
CreationDate = organizationIntegrationConfiguration.CreationDate;
|
||||
EventType = organizationIntegrationConfiguration.EventType;
|
||||
Template = organizationIntegrationConfiguration.Template;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
public string? Configuration { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
public EventType EventType { get; set; }
|
||||
public string? Template { get; set; }
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
|
||||
public class OrganizationIntegrationResponseModel : ResponseModel
|
||||
{
|
||||
public OrganizationIntegrationResponseModel(OrganizationIntegration organizationIntegration, string obj = "organizationIntegration")
|
||||
: base(obj)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(organizationIntegration);
|
||||
|
||||
Id = organizationIntegration.Id;
|
||||
Type = organizationIntegration.Type;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
public IntegrationType Type { get; set; }
|
||||
}
|
@ -64,6 +64,7 @@ public class OrganizationResponseModel : ResponseModel
|
||||
LimitItemDeletion = organization.LimitItemDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||
UseRiskInsights = organization.UseRiskInsights;
|
||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
@ -110,6 +111,7 @@ public class OrganizationResponseModel : ResponseModel
|
||||
public bool LimitItemDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
}
|
||||
|
||||
public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
|
||||
|
@ -72,6 +72,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||
UserIsClaimedByOrganization = organizationIdsClaimingUser.Contains(organization.OrganizationId);
|
||||
UseRiskInsights = organization.UseRiskInsights;
|
||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||
|
||||
if (organization.SsoConfig != null)
|
||||
{
|
||||
@ -155,4 +156,5 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
/// </returns>
|
||||
public bool UserIsClaimedByOrganization { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
}
|
||||
|
@ -50,5 +50,6 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
|
||||
LimitItemDeletion = organization.LimitItemDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||
UseRiskInsights = organization.UseRiskInsights;
|
||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||
}
|
||||
}
|
||||
|
@ -284,52 +284,6 @@ public class AccountsController : Controller
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
[HttpPost("set-key-connector-key")]
|
||||
public async Task PostSetKeyConnectorKeyAsync([FromBody] SetKeyConnectorKeyRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var result = await _userService.SetKeyConnectorKeyAsync(model.ToUser(user), model.Key, model.OrgIdentifier);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
[HttpPost("convert-to-key-connector")]
|
||||
public async Task PostConvertToKeyConnector()
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var result = await _userService.ConvertToKeyConnectorAsync(user);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
[HttpPost("kdf")]
|
||||
public async Task PostKdf([FromBody] KdfRequestModel model)
|
||||
{
|
||||
|
@ -4,6 +4,7 @@ using Bit.Api.Auth.Models.Response.WebAuthn;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Api.Response.Accounts;
|
||||
@ -31,6 +32,8 @@ public class WebAuthnController : Controller
|
||||
private readonly ICreateWebAuthnLoginCredentialCommand _createWebAuthnLoginCredentialCommand;
|
||||
private readonly IAssertWebAuthnLoginCredentialCommand _assertWebAuthnLoginCredentialCommand;
|
||||
private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public WebAuthnController(
|
||||
IUserService userService,
|
||||
@ -41,7 +44,9 @@ public class WebAuthnController : Controller
|
||||
IGetWebAuthnLoginCredentialCreateOptionsCommand getWebAuthnLoginCredentialCreateOptionsCommand,
|
||||
ICreateWebAuthnLoginCredentialCommand createWebAuthnLoginCredentialCommand,
|
||||
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand,
|
||||
IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand)
|
||||
IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_userService = userService;
|
||||
_policyService = policyService;
|
||||
@ -52,7 +57,8 @@ public class WebAuthnController : Controller
|
||||
_createWebAuthnLoginCredentialCommand = createWebAuthnLoginCredentialCommand;
|
||||
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
|
||||
_getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
@ -68,7 +74,7 @@ public class WebAuthnController : Controller
|
||||
public async Task<WebAuthnCredentialCreateOptionsResponseModel> AttestationOptions([FromBody] SecretVerificationRequestModel model)
|
||||
{
|
||||
var user = await VerifyUserAsync(model);
|
||||
await ValidateRequireSsoPolicyDisabledOrNotApplicable(user.Id);
|
||||
await ValidateIfUserCanUsePasskeyLogin(user.Id);
|
||||
var options = await _getWebAuthnLoginCredentialCreateOptionsCommand.GetWebAuthnLoginCredentialCreateOptionsAsync(user);
|
||||
|
||||
var tokenable = new WebAuthnCredentialCreateOptionsTokenable(user, options);
|
||||
@ -101,7 +107,7 @@ public class WebAuthnController : Controller
|
||||
public async Task Post([FromBody] WebAuthnLoginCredentialCreateRequestModel model)
|
||||
{
|
||||
var user = await GetUserAsync();
|
||||
await ValidateRequireSsoPolicyDisabledOrNotApplicable(user.Id);
|
||||
await ValidateIfUserCanUsePasskeyLogin(user.Id);
|
||||
var tokenable = _createOptionsDataProtector.Unprotect(model.Token);
|
||||
|
||||
if (!tokenable.TokenIsValid(user))
|
||||
@ -126,6 +132,22 @@ public class WebAuthnController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidateIfUserCanUsePasskeyLogin(Guid userId)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
|
||||
{
|
||||
await ValidateRequireSsoPolicyDisabledOrNotApplicable(userId);
|
||||
return;
|
||||
}
|
||||
|
||||
var requireSsoPolicyRequirement = await _policyRequirementQuery.GetAsync<RequireSsoPolicyRequirement>(userId);
|
||||
|
||||
if (!requireSsoPolicyRequirement.CanUsePasskeyLogin)
|
||||
{
|
||||
throw new BadRequestException("Passkeys cannot be created for your account. SSO login is required.");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut()]
|
||||
public async Task UpdateCredential([FromBody] WebAuthnLoginCredentialUpdateRequestModel model)
|
||||
{
|
||||
|
@ -84,10 +84,27 @@ public class OrganizationSponsorshipsController : Controller
|
||||
throw new BadRequestException("Free Bitwarden Families sponsorship has been disabled by your organization administrator.");
|
||||
}
|
||||
|
||||
if (!_featureService.IsEnabled(Bit.Core.FeatureFlagKeys.PM17772_AdminInitiatedSponsorships))
|
||||
{
|
||||
if (model.SponsoringUserId.HasValue)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(model.Notes))
|
||||
{
|
||||
model.Notes = null;
|
||||
}
|
||||
}
|
||||
|
||||
var targetUser = model.SponsoringUserId ?? _currentContext.UserId!.Value;
|
||||
var sponsorship = await _createSponsorshipCommand.CreateSponsorshipAsync(
|
||||
sponsoringOrg,
|
||||
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default),
|
||||
model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName);
|
||||
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, targetUser),
|
||||
model.PlanSponsorshipType,
|
||||
model.SponsoredEmail,
|
||||
model.FriendlyName,
|
||||
model.Notes);
|
||||
await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name);
|
||||
}
|
||||
|
||||
|
@ -128,6 +128,7 @@ public class DevicesController : Controller
|
||||
}
|
||||
|
||||
[HttpPost("{identifier}/retrieve-keys")]
|
||||
[Obsolete("This endpoint is deprecated. The keys are on the regular device GET endpoints now.")]
|
||||
public async Task<ProtectedDeviceResponseModel> GetDeviceKeys(string identifier)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
|
@ -3,6 +3,7 @@ using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -20,6 +21,7 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
|
||||
private readonly ICreateSponsorshipCommand _offerSponsorshipCommand;
|
||||
private readonly IRevokeSponsorshipCommand _revokeSponsorshipCommand;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public SelfHostedOrganizationSponsorshipsController(
|
||||
ICreateSponsorshipCommand offerSponsorshipCommand,
|
||||
@ -27,7 +29,8 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ICurrentContext currentContext
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService
|
||||
)
|
||||
{
|
||||
_offerSponsorshipCommand = offerSponsorshipCommand;
|
||||
@ -36,15 +39,29 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
|
||||
_organizationSponsorshipRepository = organizationSponsorshipRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_currentContext = currentContext;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
[HttpPost("{sponsoringOrgId}/families-for-enterprise")]
|
||||
public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] OrganizationSponsorshipCreateRequestModel model)
|
||||
{
|
||||
if (!_featureService.IsEnabled(Bit.Core.FeatureFlagKeys.PM17772_AdminInitiatedSponsorships))
|
||||
{
|
||||
if (model.SponsoringUserId.HasValue)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(model.Notes))
|
||||
{
|
||||
model.Notes = null;
|
||||
}
|
||||
}
|
||||
|
||||
await _offerSponsorshipCommand.CreateSponsorshipAsync(
|
||||
await _organizationRepository.GetByIdAsync(sponsoringOrgId),
|
||||
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default),
|
||||
model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName);
|
||||
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, model.SponsoringUserId ?? _currentContext.UserId ?? default),
|
||||
model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName, model.Notes);
|
||||
}
|
||||
|
||||
[HttpDelete("{sponsoringOrgId}")]
|
||||
|
@ -24,7 +24,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Controllers;
|
||||
|
||||
[Route("accounts/key-management")]
|
||||
[Route("accounts")]
|
||||
[Authorize("Application")]
|
||||
public class AccountsKeyManagementController : Controller
|
||||
{
|
||||
@ -77,7 +77,7 @@ public class AccountsKeyManagementController : Controller
|
||||
_deviceValidator = deviceValidator;
|
||||
}
|
||||
|
||||
[HttpPost("regenerate-keys")]
|
||||
[HttpPost("key-management/regenerate-keys")]
|
||||
public async Task RegenerateKeysAsync([FromBody] KeyRegenerationRequestModel request)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration))
|
||||
@ -93,7 +93,7 @@ public class AccountsKeyManagementController : Controller
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("rotate-user-account-keys")]
|
||||
[HttpPost("key-management/rotate-user-account-keys")]
|
||||
public async Task RotateUserAccountKeysAsync([FromBody] RotateUserAccountKeysAndDataRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
@ -133,4 +133,50 @@ public class AccountsKeyManagementController : Controller
|
||||
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
[HttpPost("set-key-connector-key")]
|
||||
public async Task PostSetKeyConnectorKeyAsync([FromBody] SetKeyConnectorKeyRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var result = await _userService.SetKeyConnectorKeyAsync(model.ToUser(user), model.Key, model.OrgIdentifier);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
[HttpPost("convert-to-key-connector")]
|
||||
public async Task PostConvertToKeyConnectorAsync()
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var result = await _userService.ConvertToKeyConnectorAsync(user);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Api.Auth.Models.Request.Accounts;
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
|
||||
public class SetKeyConnectorKeyRequestModel
|
||||
{
|
@ -16,4 +16,14 @@ public class OrganizationSponsorshipCreateRequestModel
|
||||
|
||||
[StringLength(256)]
|
||||
public string FriendlyName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// (optional) The user to target for the sponsorship.
|
||||
/// </summary>
|
||||
/// <remarks>Left empty when creating a sponsorship for the authenticated user.</remarks>
|
||||
public Guid? SponsoringUserId { get; set; }
|
||||
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(512)]
|
||||
public string Notes { get; set; }
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.Models.Response;
|
||||
|
||||
@ -21,6 +22,8 @@ public class DeviceResponseModel : ResponseModel
|
||||
Identifier = device.Identifier;
|
||||
CreationDate = device.CreationDate;
|
||||
IsTrusted = device.IsTrusted();
|
||||
EncryptedUserKey = device.EncryptedUserKey;
|
||||
EncryptedPublicKey = device.EncryptedPublicKey;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
@ -29,4 +32,10 @@ public class DeviceResponseModel : ResponseModel
|
||||
public string Identifier { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
public bool IsTrusted { get; set; }
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(2000)]
|
||||
public string EncryptedUserKey { get; set; }
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(2000)]
|
||||
public string EncryptedPublicKey { get; set; }
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ using Bit.Core.Settings;
|
||||
using AspNetCoreRateLimit;
|
||||
using Stripe;
|
||||
using Bit.Core.Utilities;
|
||||
using Duende.IdentityModel;
|
||||
using IdentityModel;
|
||||
using System.Globalization;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Auth.Models.Request;
|
||||
@ -27,8 +27,10 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
using Bit.Core.AdminConsole.Services.NoopImplementations;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.ImportFeatures;
|
||||
using Bit.Core.Tools.ReportFeatures;
|
||||
using Bit.Core.Auth.Models.Api.Request;
|
||||
@ -216,6 +218,19 @@ public class Startup
|
||||
services.AddHostedService<Core.HostedServices.ApplicationCacheHostedService>();
|
||||
}
|
||||
|
||||
// Slack
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes))
|
||||
{
|
||||
services.AddHttpClient(SlackService.HttpClientName);
|
||||
services.AddSingleton<ISlackService, SlackService>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<ISlackService, NoopSlackService>();
|
||||
}
|
||||
|
||||
// This should be registered last because it customizes the primary http message handler and we want it to win.
|
||||
services.AddX509ChainCustomization();
|
||||
}
|
||||
|
@ -77,10 +77,9 @@ public class ImportCiphersController : Controller
|
||||
|
||||
//An User is allowed to import if CanCreate Collections or has AccessToImportExport
|
||||
var authorized = await CheckOrgImportPermission(collections, orgId);
|
||||
|
||||
if (!authorized)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
throw new BadRequestException("Not enough privileges to import into this organization.");
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
@ -103,21 +102,59 @@ public class ImportCiphersController : Controller
|
||||
.Select(c => c.Id)
|
||||
.ToHashSet();
|
||||
|
||||
//We need to verify if the user is trying to import into existing collections
|
||||
var existingCollections = collections.Where(tc => orgCollectionIds.Contains(tc.Id));
|
||||
|
||||
//When importing into existing collection, we need to verify if the user has permissions
|
||||
if (existingCollections.Any() && !(await _authorizationService.AuthorizeAsync(User, existingCollections, BulkCollectionOperations.ImportCiphers)).Succeeded)
|
||||
// when there are no collections, then we can import
|
||||
if (collections.Count == 0)
|
||||
{
|
||||
return false;
|
||||
};
|
||||
|
||||
//Users allowed to import if they CanCreate Collections
|
||||
if (!(await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.Create)).Succeeded)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// are we trying to import into existing collections?
|
||||
var existingCollections = collections.Where(tc => orgCollectionIds.Contains(tc.Id));
|
||||
|
||||
// are we trying to create new collections?
|
||||
var hasNewCollections = collections.Any(tc => !orgCollectionIds.Contains(tc.Id));
|
||||
|
||||
// suppose we have both new and existing collections
|
||||
if (hasNewCollections && existingCollections.Any())
|
||||
{
|
||||
// since we are creating new collection, user must have import/manage and create collection permission
|
||||
if ((await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.Create)).Succeeded
|
||||
&& (await _authorizationService.AuthorizeAsync(User, existingCollections, BulkCollectionOperations.ImportCiphers)).Succeeded)
|
||||
{
|
||||
// can import collections and create new ones
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// user does not have permission to import
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// suppose we have new collections and none of our collections exist
|
||||
if (hasNewCollections && !existingCollections.Any())
|
||||
{
|
||||
// user is trying to create new collections
|
||||
// we need to check if the user has permission to create collections
|
||||
if ((await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.Create)).Succeeded)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// user does not have permission to create new collections
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// in many import formats, we don't create collections, we just import ciphers into an existing collection
|
||||
|
||||
// When importing, we need to verify if the user has ImportCiphers permission
|
||||
if (existingCollections.Any() && (await _authorizationService.AuthorizeAsync(User, existingCollections, BulkCollectionOperations.ImportCiphers)).Succeeded)
|
||||
{
|
||||
return true;
|
||||
};
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Api.Tools.Authorization;
|
||||
using Bit.Api.AdminConsole.Authorization;
|
||||
using Bit.Api.Tools.Authorization;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
|
||||
using Bit.Core.IdentityServer;
|
||||
@ -105,5 +106,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<IAuthorizationHandler, VaultExportAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, SecurityTaskAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, SecurityTaskOrganizationAuthorizationHandler>();
|
||||
|
||||
services.AddScoped<IAuthorizationHandler, OrganizationRequirementHandler>();
|
||||
}
|
||||
}
|
||||
|
@ -177,12 +177,7 @@ public class CiphersController : Controller
|
||||
}
|
||||
|
||||
await _cipherService.SaveDetailsAsync(cipher, user.Id, model.Cipher.LastKnownRevisionDate, model.CollectionIds, cipher.OrganizationId.HasValue);
|
||||
var response = new CipherResponseModel(
|
||||
cipher,
|
||||
user,
|
||||
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
|
||||
_globalSettings);
|
||||
return response;
|
||||
return await Get(cipher.Id);
|
||||
}
|
||||
|
||||
[HttpPost("admin")]
|
||||
|
@ -16,6 +16,12 @@ public interface IStripeFacade
|
||||
RequestOptions requestOptions = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<Customer> UpdateCustomer(
|
||||
string customerId,
|
||||
CustomerUpdateOptions customerUpdateOptions = null,
|
||||
RequestOptions requestOptions = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<Event> GetEvent(
|
||||
string eventId,
|
||||
EventGetOptions eventGetOptions = null,
|
||||
|
@ -1,4 +1,10 @@
|
||||
using Bit.Billing.Constants;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Services;
|
||||
using Stripe;
|
||||
using Event = Stripe.Event;
|
||||
|
||||
@ -10,20 +16,124 @@ public class PaymentMethodAttachedHandler : IPaymentMethodAttachedHandler
|
||||
private readonly IStripeEventService _stripeEventService;
|
||||
private readonly IStripeFacade _stripeFacade;
|
||||
private readonly IStripeEventUtilityService _stripeEventUtilityService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
|
||||
public PaymentMethodAttachedHandler(
|
||||
ILogger<PaymentMethodAttachedHandler> logger,
|
||||
IStripeEventService stripeEventService,
|
||||
IStripeFacade stripeFacade,
|
||||
IStripeEventUtilityService stripeEventUtilityService)
|
||||
IStripeEventUtilityService stripeEventUtilityService,
|
||||
IFeatureService featureService,
|
||||
IProviderRepository providerRepository)
|
||||
{
|
||||
_logger = logger;
|
||||
_stripeEventService = stripeEventService;
|
||||
_stripeFacade = stripeFacade;
|
||||
_stripeEventUtilityService = stripeEventUtilityService;
|
||||
_featureService = featureService;
|
||||
_providerRepository = providerRepository;
|
||||
}
|
||||
|
||||
public async Task HandleAsync(Event parsedEvent)
|
||||
{
|
||||
var updateMSPToChargeAutomatically =
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically);
|
||||
|
||||
if (updateMSPToChargeAutomatically)
|
||||
{
|
||||
await HandleVNextAsync(parsedEvent);
|
||||
}
|
||||
else
|
||||
{
|
||||
await HandleVCurrentAsync(parsedEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleVNextAsync(Event parsedEvent)
|
||||
{
|
||||
var paymentMethod = await _stripeEventService.GetPaymentMethod(parsedEvent, true, ["customer.subscriptions.data.latest_invoice"]);
|
||||
|
||||
if (paymentMethod == null)
|
||||
{
|
||||
_logger.LogWarning("Attempted to handle the event payment_method.attached but paymentMethod was null");
|
||||
return;
|
||||
}
|
||||
|
||||
var customer = paymentMethod.Customer;
|
||||
var subscriptions = customer?.Subscriptions;
|
||||
|
||||
// This represents a provider subscription set to "send_invoice" that was paid using a Stripe hosted invoice payment page.
|
||||
var invoicedProviderSubscription = subscriptions?.Data.FirstOrDefault(subscription =>
|
||||
subscription.Metadata.ContainsKey(StripeConstants.MetadataKeys.ProviderId) &&
|
||||
subscription.Status != StripeConstants.SubscriptionStatus.Canceled &&
|
||||
subscription.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice);
|
||||
|
||||
/*
|
||||
* If we have an invoiced provider subscription where the customer hasn't been marked as invoice-approved,
|
||||
* we need to try and set the default payment method and update the collection method to be "charge_automatically".
|
||||
*/
|
||||
if (invoicedProviderSubscription != null &&
|
||||
!customer.ApprovedToPayByInvoice() &&
|
||||
Guid.TryParse(invoicedProviderSubscription.Metadata[StripeConstants.MetadataKeys.ProviderId], out var providerId))
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||
|
||||
if (provider is { Type: ProviderType.Msp })
|
||||
{
|
||||
if (customer.InvoiceSettings.DefaultPaymentMethodId != paymentMethod.Id)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _stripeFacade.UpdateCustomer(customer.Id,
|
||||
new CustomerUpdateOptions
|
||||
{
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
DefaultPaymentMethod = paymentMethod.Id
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogWarning(exception,
|
||||
"Failed to set customer's ({CustomerID}) default payment method during 'payment_method.attached' webhook",
|
||||
customer.Id);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _stripeFacade.UpdateSubscription(invoicedProviderSubscription.Id,
|
||||
new SubscriptionUpdateOptions
|
||||
{
|
||||
CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically
|
||||
});
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogWarning(exception,
|
||||
"Failed to set subscription's ({SubscriptionID}) collection method to 'charge_automatically' during 'payment_method.attached' webhook",
|
||||
customer.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var unpaidSubscriptions = subscriptions?.Data.Where(subscription =>
|
||||
subscription.Status == StripeConstants.SubscriptionStatus.Unpaid).ToList();
|
||||
|
||||
if (unpaidSubscriptions == null || unpaidSubscriptions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var unpaidSubscription in unpaidSubscriptions)
|
||||
{
|
||||
await AttemptToPayOpenSubscriptionAsync(unpaidSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleVCurrentAsync(Event parsedEvent)
|
||||
{
|
||||
var paymentMethod = await _stripeEventService.GetPaymentMethod(parsedEvent);
|
||||
if (paymentMethod is null)
|
||||
|
@ -33,6 +33,13 @@ public class StripeFacade : IStripeFacade
|
||||
CancellationToken cancellationToken = default) =>
|
||||
await _customerService.GetAsync(customerId, customerGetOptions, requestOptions, cancellationToken);
|
||||
|
||||
public async Task<Customer> UpdateCustomer(
|
||||
string customerId,
|
||||
CustomerUpdateOptions customerUpdateOptions = null,
|
||||
RequestOptions requestOptions = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
await _customerService.UpdateAsync(customerId, customerUpdateOptions, requestOptions, cancellationToken);
|
||||
|
||||
public async Task<Invoice> GetInvoice(
|
||||
string invoiceId,
|
||||
InvoiceGetOptions invoiceGetOptions = null,
|
||||
|
@ -114,6 +114,11 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
||||
/// </summary>
|
||||
public bool UseRiskInsights { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set to true, admins can initiate organization-issued sponsorships.
|
||||
/// </summary>
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
if (Id == default(Guid))
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
public enum IntegrationType : int
|
||||
{
|
||||
Slack = 1,
|
||||
Webhook = 2,
|
||||
CloudBillingSync = 1,
|
||||
Scim = 2,
|
||||
Slack = 3,
|
||||
Webhook = 4,
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
namespace Bit.Core.Models.Data.Integrations;
|
||||
|
||||
public record SlackIntegration(string token);
|
@ -0,0 +1,3 @@
|
||||
namespace Bit.Core.Models.Data.Integrations;
|
||||
|
||||
public record SlackIntegrationConfiguration(string channelId);
|
@ -0,0 +1,3 @@
|
||||
namespace Bit.Core.Models.Data.Integrations;
|
||||
|
||||
public record SlackIntegrationConfigurationDetails(string channelId, string token);
|
@ -0,0 +1,3 @@
|
||||
namespace Bit.Core.Models.Data.Integrations;
|
||||
|
||||
public record WebhookIntegrationConfiguration(string url);
|
@ -0,0 +1,3 @@
|
||||
namespace Bit.Core.Models.Data.Integrations;
|
||||
|
||||
public record WebhookIntegrationConfigurationDetils(string url);
|
@ -26,6 +26,7 @@ public class OrganizationAbility
|
||||
LimitItemDeletion = organization.LimitItemDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||
UseRiskInsights = organization.UseRiskInsights;
|
||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
@ -45,4 +46,5 @@ public class OrganizationAbility
|
||||
public bool LimitItemDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
}
|
||||
|
@ -59,4 +59,5 @@ public class OrganizationUserOrganizationDetails
|
||||
public bool LimitItemDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.Identity;
|
||||
|
||||
namespace Bit.Core.Models.Data;
|
||||
|
||||
@ -20,17 +21,17 @@ public class Permissions
|
||||
[JsonIgnore]
|
||||
public List<(bool Permission, string ClaimName)> ClaimsMap => new()
|
||||
{
|
||||
(AccessEventLogs, "accesseventlogs"),
|
||||
(AccessImportExport, "accessimportexport"),
|
||||
(AccessReports, "accessreports"),
|
||||
(CreateNewCollections, "createnewcollections"),
|
||||
(EditAnyCollection, "editanycollection"),
|
||||
(DeleteAnyCollection, "deleteanycollection"),
|
||||
(ManageGroups, "managegroups"),
|
||||
(ManagePolicies, "managepolicies"),
|
||||
(ManageSso, "managesso"),
|
||||
(ManageUsers, "manageusers"),
|
||||
(ManageResetPassword, "manageresetpassword"),
|
||||
(ManageScim, "managescim"),
|
||||
(AccessEventLogs, Claims.CustomPermissions.AccessEventLogs),
|
||||
(AccessImportExport, Claims.CustomPermissions.AccessImportExport),
|
||||
(AccessReports, Claims.CustomPermissions.AccessReports),
|
||||
(CreateNewCollections, Claims.CustomPermissions.CreateNewCollections),
|
||||
(EditAnyCollection, Claims.CustomPermissions.EditAnyCollection),
|
||||
(DeleteAnyCollection, Claims.CustomPermissions.DeleteAnyCollection),
|
||||
(ManageGroups, Claims.CustomPermissions.ManageGroups),
|
||||
(ManagePolicies, Claims.CustomPermissions.ManagePolicies),
|
||||
(ManageSso, Claims.CustomPermissions.ManageSso),
|
||||
(ManageUsers, Claims.CustomPermissions.ManageUsers),
|
||||
(ManageResetPassword, Claims.CustomPermissions.ManageResetPassword),
|
||||
(ManageScim, Claims.CustomPermissions.ManageScim),
|
||||
};
|
||||
}
|
||||
|
@ -45,5 +45,6 @@ public class ProviderUserOrganizationDetails
|
||||
public bool LimitItemDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public ProviderType ProviderType { get; set; }
|
||||
}
|
||||
|
57
src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs
Normal file
57
src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs
Normal file
@ -0,0 +1,57 @@
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Bit.Core.Models.Slack;
|
||||
|
||||
public abstract class SlackApiResponse
|
||||
{
|
||||
public bool Ok { get; set; }
|
||||
[JsonPropertyName("response_metadata")]
|
||||
public SlackResponseMetadata ResponseMetadata { get; set; } = new();
|
||||
public string Error { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class SlackResponseMetadata
|
||||
{
|
||||
[JsonPropertyName("next_cursor")]
|
||||
public string NextCursor { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class SlackChannelListResponse : SlackApiResponse
|
||||
{
|
||||
public List<SlackChannel> Channels { get; set; } = new();
|
||||
}
|
||||
|
||||
public class SlackUserResponse : SlackApiResponse
|
||||
{
|
||||
public SlackUser User { get; set; } = new();
|
||||
}
|
||||
|
||||
public class SlackOAuthResponse : SlackApiResponse
|
||||
{
|
||||
[JsonPropertyName("access_token")]
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public SlackTeam Team { get; set; } = new();
|
||||
}
|
||||
|
||||
public class SlackTeam
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class SlackChannel
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class SlackUser
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class SlackDmResponse : SlackApiResponse
|
||||
{
|
||||
public SlackChannel Channel { get; set; } = new();
|
||||
}
|
@ -159,13 +159,13 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
||||
|
||||
private async Task RevertPasswordManagerChangesAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
||||
{
|
||||
if (validatedResult.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd > 0)
|
||||
if (validatedResult.Value.PasswordManagerSubscriptionUpdate is { Seats: > 0, SeatsRequiredToAdd: > 0 })
|
||||
{
|
||||
// When reverting seats, we have to tell payments service that the seats are going back down by what we attempted to add.
|
||||
// However, this might lead to a problem if we don't actually update stripe but throw any ways.
|
||||
// stripe could not be updated, and then we would decrement the number of seats in stripe accidentally.
|
||||
var seatsToRemove = validatedResult.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd;
|
||||
await paymentService.AdjustSeatsAsync(organization, validatedResult.Value.InviteOrganization.Plan, -seatsToRemove);
|
||||
|
||||
|
||||
await paymentService.AdjustSeatsAsync(organization,
|
||||
validatedResult.Value.InviteOrganization.Plan,
|
||||
validatedResult.Value.PasswordManagerSubscriptionUpdate.Seats.Value);
|
||||
|
||||
organization.Seats = (short?)validatedResult.Value.PasswordManagerSubscriptionUpdate.Seats;
|
||||
|
||||
@ -206,10 +206,26 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
||||
|
||||
private async Task SendAdditionalEmailsAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
||||
{
|
||||
await SendPasswordManagerMaxSeatLimitEmailsAsync(validatedResult, organization);
|
||||
await NotifyOwnersIfAutoscaleOccursAsync(validatedResult, organization);
|
||||
await NotifyOwnersIfPasswordManagerMaxSeatLimitReachedAsync(validatedResult, organization);
|
||||
}
|
||||
|
||||
private async Task SendPasswordManagerMaxSeatLimitEmailsAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
||||
private async Task NotifyOwnersIfAutoscaleOccursAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
||||
{
|
||||
if (validatedResult.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd > 0
|
||||
&& !organization.OwnersNotifiedOfAutoscaling.HasValue)
|
||||
{
|
||||
await mailService.SendOrganizationAutoscaledEmailAsync(
|
||||
organization,
|
||||
validatedResult.Value.PasswordManagerSubscriptionUpdate.Seats!.Value,
|
||||
await GetOwnerEmailAddressesAsync(validatedResult.Value.InviteOrganization));
|
||||
|
||||
organization.OwnersNotifiedOfAutoscaling = validatedResult.Value.PerformedAt.UtcDateTime;
|
||||
await organizationRepository.UpsertAsync(organization);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task NotifyOwnersIfPasswordManagerMaxSeatLimitReachedAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
||||
{
|
||||
if (!validatedResult.Value.PasswordManagerSubscriptionUpdate.MaxSeatsReached)
|
||||
{
|
||||
@ -258,12 +274,11 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
||||
|
||||
private async Task AdjustPasswordManagerSeatsAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
||||
{
|
||||
if (validatedResult.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd <= 0)
|
||||
if (validatedResult.Value.PasswordManagerSubscriptionUpdate is { SeatsRequiredToAdd: > 0, UpdatedSeatTotal: > 0 })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await paymentService.AdjustSeatsAsync(organization, validatedResult.Value.InviteOrganization.Plan, validatedResult.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd);
|
||||
await paymentService.AdjustSeatsAsync(organization,
|
||||
validatedResult.Value.InviteOrganization.Plan,
|
||||
validatedResult.Value.PasswordManagerSubscriptionUpdate.UpdatedSeatTotal.Value);
|
||||
|
||||
organization.Seats = (short?)validatedResult.Value.PasswordManagerSubscriptionUpdate.UpdatedSeatTotal;
|
||||
|
||||
@ -279,4 +294,5 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
||||
PreviousSeats = validatedResult.Value.PasswordManagerSubscriptionUpdate.Seats
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,62 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Policy requirements for the Require SSO policy.
|
||||
/// </summary>
|
||||
public class RequireSsoPolicyRequirement : IPolicyRequirement
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates whether the user can use passkey login.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The user can use passkey login if they are not a member (Accepted/Confirmed) of an organization
|
||||
/// that has the Require SSO policy enabled.
|
||||
/// </remarks>
|
||||
public bool CanUsePasskeyLogin { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether SSO requirement is enforced for the user.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The user is required to login with SSO if they are a confirmed member of an organization
|
||||
/// that has the Require SSO policy enabled.
|
||||
/// </remarks>
|
||||
public bool SsoRequired { get; init; }
|
||||
}
|
||||
|
||||
|
||||
public class RequireSsoPolicyRequirementFactory : BasePolicyRequirementFactory<RequireSsoPolicyRequirement>
|
||||
{
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public RequireSsoPolicyRequirementFactory(GlobalSettings globalSettings)
|
||||
{
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public override PolicyType PolicyType => PolicyType.RequireSso;
|
||||
|
||||
protected override IEnumerable<OrganizationUserType> ExemptRoles =>
|
||||
_globalSettings.Sso.EnforceSsoPolicyForAllUsers
|
||||
? Array.Empty<OrganizationUserType>()
|
||||
: [OrganizationUserType.Owner, OrganizationUserType.Admin];
|
||||
|
||||
public override RequireSsoPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
|
||||
{
|
||||
var result = new RequireSsoPolicyRequirement
|
||||
{
|
||||
CanUsePasskeyLogin = policyDetails.All(p =>
|
||||
p.OrganizationUserStatus == OrganizationUserStatusType.Revoked ||
|
||||
p.OrganizationUserStatus == OrganizationUserStatusType.Invited),
|
||||
|
||||
SsoRequired = policyDetails.Any(p =>
|
||||
p.OrganizationUserStatus == OrganizationUserStatusType.Confirmed)
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -35,5 +35,6 @@ public static class PolicyServiceCollectionExtensions
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, SendOptionsPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, ResetPasswordPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, PersonalOwnershipPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireSsoPolicyRequirementFactory>();
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ public interface IOrganizationService
|
||||
/// <remarks>
|
||||
/// This method must target a disabled Organization that has null keys and status as 'Pending'.
|
||||
/// </remarks>
|
||||
Task InitPendingOrganization(Guid userId, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName);
|
||||
Task InitPendingOrganization(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken);
|
||||
Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null);
|
||||
|
||||
void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade);
|
||||
|
11
src/Core/AdminConsole/Services/ISlackService.cs
Normal file
11
src/Core/AdminConsole/Services/ISlackService.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
public interface ISlackService
|
||||
{
|
||||
Task<string> GetChannelIdAsync(string token, string channelName);
|
||||
Task<List<string>> GetChannelIdsAsync(string token, List<string> channelNames);
|
||||
Task<string> GetDmChannelByEmailAsync(string token, string email);
|
||||
string GetRedirectUrl(string redirectUrl);
|
||||
Task<string> ObtainTokenViaOAuth(string code, string redirectUrl);
|
||||
Task SendSlackMessageByChannelIdAsync(string token, string message, string channelId);
|
||||
}
|
@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Constants;
|
||||
@ -30,10 +31,12 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Stripe;
|
||||
using OrganizationUserInvite = Bit.Core.Models.Business.OrganizationUserInvite;
|
||||
@ -74,6 +77,8 @@ public class OrganizationService : IOrganizationService
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand;
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||
private readonly IDataProtector _dataProtector;
|
||||
|
||||
public OrganizationService(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -107,7 +112,10 @@ public class OrganizationService : IOrganizationService
|
||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
||||
IPricingClient pricingClient,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
ISendOrganizationInvitesCommand sendOrganizationInvitesCommand)
|
||||
ISendOrganizationInvitesCommand sendOrganizationInvitesCommand,
|
||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
|
||||
IDataProtectionProvider dataProtectionProvider
|
||||
)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -141,6 +149,8 @@ public class OrganizationService : IOrganizationService
|
||||
_pricingClient = pricingClient;
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
_sendOrganizationInvitesCommand = sendOrganizationInvitesCommand;
|
||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||
_dataProtector = dataProtectionProvider.CreateProtector(OrgUserInviteTokenable.DataProtectorPurpose);
|
||||
}
|
||||
|
||||
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
|
||||
@ -1912,9 +1922,28 @@ public class OrganizationService : IOrganizationService
|
||||
});
|
||||
}
|
||||
|
||||
public async Task InitPendingOrganization(Guid userId, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName)
|
||||
public async Task InitPendingOrganization(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken)
|
||||
{
|
||||
await ValidateSignUpPoliciesAsync(userId);
|
||||
await ValidateSignUpPoliciesAsync(user.Id);
|
||||
|
||||
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
||||
if (orgUser == null)
|
||||
{
|
||||
throw new BadRequestException("User invalid.");
|
||||
}
|
||||
|
||||
// TODO: PM-4142 - remove old token validation logic once 3 releases of backwards compatibility are complete
|
||||
var newTokenValid = OrgUserInviteTokenable.ValidateOrgUserInviteStringToken(
|
||||
_orgUserInviteTokenDataFactory, emailToken, orgUser);
|
||||
|
||||
var tokenValid = newTokenValid ||
|
||||
CoreHelpers.UserInviteTokenIsValid(_dataProtector, emailToken, user.Email, orgUser.Id,
|
||||
_globalSettings);
|
||||
|
||||
if (!tokenValid)
|
||||
{
|
||||
throw new BadRequestException("Invalid token.");
|
||||
}
|
||||
|
||||
var org = await GetOrgById(organizationId);
|
||||
|
||||
|
@ -0,0 +1,46 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Utilities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Integrations;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
public class SlackEventHandler(
|
||||
IOrganizationIntegrationConfigurationRepository configurationRepository,
|
||||
ISlackService slackService)
|
||||
: IEventMessageHandler
|
||||
{
|
||||
public async Task HandleEventAsync(EventMessage eventMessage)
|
||||
{
|
||||
var organizationId = eventMessage.OrganizationId ?? Guid.Empty;
|
||||
var configurations = await configurationRepository.GetConfigurationDetailsAsync(
|
||||
organizationId,
|
||||
IntegrationType.Slack,
|
||||
eventMessage.Type);
|
||||
|
||||
foreach (var configuration in configurations)
|
||||
{
|
||||
var config = configuration.MergedConfiguration.Deserialize<SlackIntegrationConfigurationDetails>();
|
||||
if (config is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await slackService.SendSlackMessageByChannelIdAsync(
|
||||
config.token,
|
||||
IntegrationTemplateProcessor.ReplaceTokens(configuration.Template, eventMessage),
|
||||
config.channelId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task HandleManyEventsAsync(IEnumerable<EventMessage> eventMessages)
|
||||
{
|
||||
foreach (var eventMessage in eventMessages)
|
||||
{
|
||||
await HandleEventAsync(eventMessage);
|
||||
}
|
||||
}
|
||||
}
|
162
src/Core/AdminConsole/Services/Implementations/SlackService.cs
Normal file
162
src/Core/AdminConsole/Services/Implementations/SlackService.cs
Normal file
@ -0,0 +1,162 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Web;
|
||||
using Bit.Core.Models.Slack;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
public class SlackService(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
GlobalSettings globalSettings,
|
||||
ILogger<SlackService> logger) : ISlackService
|
||||
{
|
||||
private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName);
|
||||
private readonly string _clientId = globalSettings.Slack.ClientId;
|
||||
private readonly string _clientSecret = globalSettings.Slack.ClientSecret;
|
||||
private readonly string _scopes = globalSettings.Slack.Scopes;
|
||||
private readonly string _slackApiBaseUrl = globalSettings.Slack.ApiBaseUrl;
|
||||
|
||||
public const string HttpClientName = "SlackServiceHttpClient";
|
||||
|
||||
public async Task<string> GetChannelIdAsync(string token, string channelName)
|
||||
{
|
||||
return (await GetChannelIdsAsync(token, [channelName])).FirstOrDefault();
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetChannelIdsAsync(string token, List<string> channelNames)
|
||||
{
|
||||
var matchingChannelIds = new List<string>();
|
||||
var baseUrl = $"{_slackApiBaseUrl}/conversations.list";
|
||||
var nextCursor = string.Empty;
|
||||
|
||||
do
|
||||
{
|
||||
var uriBuilder = new UriBuilder(baseUrl);
|
||||
var queryParameters = HttpUtility.ParseQueryString(uriBuilder.Query);
|
||||
queryParameters["types"] = "public_channel,private_channel";
|
||||
queryParameters["limit"] = "1000";
|
||||
if (!string.IsNullOrEmpty(nextCursor))
|
||||
{
|
||||
queryParameters["cursor"] = nextCursor;
|
||||
}
|
||||
uriBuilder.Query = queryParameters.ToString();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, uriBuilder.Uri);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var result = await response.Content.ReadFromJsonAsync<SlackChannelListResponse>();
|
||||
|
||||
if (result is { Ok: true })
|
||||
{
|
||||
matchingChannelIds.AddRange(result.Channels
|
||||
.Where(channel => channelNames.Contains(channel.Name))
|
||||
.Select(channel => channel.Id));
|
||||
nextCursor = result.ResponseMetadata.NextCursor;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError("Error getting Channel Ids: {Error}", result.Error);
|
||||
nextCursor = string.Empty;
|
||||
}
|
||||
|
||||
} while (!string.IsNullOrEmpty(nextCursor));
|
||||
|
||||
return matchingChannelIds;
|
||||
}
|
||||
|
||||
public async Task<string> GetDmChannelByEmailAsync(string token, string email)
|
||||
{
|
||||
var userId = await GetUserIdByEmailAsync(token, email);
|
||||
return await OpenDmChannel(token, userId);
|
||||
}
|
||||
|
||||
public string GetRedirectUrl(string redirectUrl)
|
||||
{
|
||||
return $"https://slack.com/oauth/v2/authorize?client_id={_clientId}&scope={_scopes}&redirect_uri={redirectUrl}";
|
||||
}
|
||||
|
||||
public async Task<string> ObtainTokenViaOAuth(string code, string redirectUrl)
|
||||
{
|
||||
var tokenResponse = await _httpClient.PostAsync($"{_slackApiBaseUrl}/oauth.v2.access",
|
||||
new FormUrlEncodedContent(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("client_id", _clientId),
|
||||
new KeyValuePair<string, string>("client_secret", _clientSecret),
|
||||
new KeyValuePair<string, string>("code", code),
|
||||
new KeyValuePair<string, string>("redirect_uri", redirectUrl)
|
||||
}));
|
||||
|
||||
SlackOAuthResponse result;
|
||||
try
|
||||
{
|
||||
result = await tokenResponse.Content.ReadFromJsonAsync<SlackOAuthResponse>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
result = null;
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
logger.LogError("Error obtaining token via OAuth: Unknown error");
|
||||
return string.Empty;
|
||||
}
|
||||
if (!result.Ok)
|
||||
{
|
||||
logger.LogError("Error obtaining token via OAuth: {Error}", result.Error);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return result.AccessToken;
|
||||
}
|
||||
|
||||
public async Task SendSlackMessageByChannelIdAsync(string token, string message, string channelId)
|
||||
{
|
||||
var payload = JsonContent.Create(new { channel = channelId, text = message });
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, $"{_slackApiBaseUrl}/chat.postMessage");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
request.Content = payload;
|
||||
|
||||
await _httpClient.SendAsync(request);
|
||||
}
|
||||
|
||||
private async Task<string> GetUserIdByEmailAsync(string token, string email)
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, $"{_slackApiBaseUrl}/users.lookupByEmail?email={email}");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var result = await response.Content.ReadFromJsonAsync<SlackUserResponse>();
|
||||
|
||||
if (!result.Ok)
|
||||
{
|
||||
logger.LogError("Error retrieving Slack user ID: {Error}", result.Error);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return result.User.Id;
|
||||
}
|
||||
|
||||
private async Task<string> OpenDmChannel(string token, string userId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
return string.Empty;
|
||||
|
||||
var payload = JsonContent.Create(new { users = userId });
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, $"{_slackApiBaseUrl}/conversations.open");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
request.Content = payload;
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var result = await response.Content.ReadFromJsonAsync<SlackDmResponse>();
|
||||
|
||||
if (!result.Ok)
|
||||
{
|
||||
logger.LogError("Error opening DM channel: {Error}", result.Error);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return result.Channel.Id;
|
||||
}
|
||||
}
|
@ -1,30 +1,57 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Utilities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Models.Data.Integrations;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
public class WebhookEventHandler(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
GlobalSettings globalSettings)
|
||||
IOrganizationIntegrationConfigurationRepository configurationRepository)
|
||||
: IEventMessageHandler
|
||||
{
|
||||
private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName);
|
||||
private readonly string _webhookUrl = globalSettings.EventLogging.WebhookUrl;
|
||||
|
||||
public const string HttpClientName = "WebhookEventHandlerHttpClient";
|
||||
|
||||
public async Task HandleEventAsync(EventMessage eventMessage)
|
||||
{
|
||||
var content = JsonContent.Create(eventMessage);
|
||||
var response = await _httpClient.PostAsync(_webhookUrl, content);
|
||||
var organizationId = eventMessage.OrganizationId ?? Guid.Empty;
|
||||
var configurations = await configurationRepository.GetConfigurationDetailsAsync(
|
||||
organizationId,
|
||||
IntegrationType.Webhook,
|
||||
eventMessage.Type);
|
||||
|
||||
foreach (var configuration in configurations)
|
||||
{
|
||||
var config = configuration.MergedConfiguration.Deserialize<WebhookIntegrationConfigurationDetils>();
|
||||
if (config is null || string.IsNullOrEmpty(config.url))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var content = new StringContent(
|
||||
IntegrationTemplateProcessor.ReplaceTokens(configuration.Template, eventMessage),
|
||||
Encoding.UTF8,
|
||||
"application/json"
|
||||
);
|
||||
var response = await _httpClient.PostAsync(
|
||||
config.url,
|
||||
content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task HandleManyEventsAsync(IEnumerable<EventMessage> eventMessages)
|
||||
{
|
||||
var content = JsonContent.Create(eventMessages);
|
||||
var response = await _httpClient.PostAsync(_webhookUrl, content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
foreach (var eventMessage in eventMessages)
|
||||
{
|
||||
await HandleEventAsync(eventMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,36 @@
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Services.NoopImplementations;
|
||||
|
||||
public class NoopSlackService : ISlackService
|
||||
{
|
||||
public Task<string> GetChannelIdAsync(string token, string channelName)
|
||||
{
|
||||
return Task.FromResult(string.Empty);
|
||||
}
|
||||
|
||||
public Task<List<string>> GetChannelIdsAsync(string token, List<string> channelNames)
|
||||
{
|
||||
return Task.FromResult(new List<string>());
|
||||
}
|
||||
|
||||
public Task<string> GetDmChannelByEmailAsync(string token, string email)
|
||||
{
|
||||
return Task.FromResult(string.Empty);
|
||||
}
|
||||
|
||||
public string GetRedirectUrl(string redirectUrl)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public Task SendSlackMessageByChannelIdAsync(string token, string message, string channelId)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task<string> ObtainTokenViaOAuth(string code, string redirectUrl)
|
||||
{
|
||||
return Task.FromResult(string.Empty);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Utilities;
|
||||
|
||||
public static partial class IntegrationTemplateProcessor
|
||||
{
|
||||
[GeneratedRegex(@"#(\w+)#")]
|
||||
private static partial Regex TokenRegex();
|
||||
|
||||
public static string ReplaceTokens(string template, object values)
|
||||
{
|
||||
if (string.IsNullOrEmpty(template) || values == null)
|
||||
return template;
|
||||
|
||||
var type = values.GetType();
|
||||
return TokenRegex().Replace(template, match =>
|
||||
{
|
||||
var propertyName = match.Groups[1].Value;
|
||||
var property = type.GetProperty(propertyName);
|
||||
return property?.GetValue(values)?.ToString() ?? match.Value;
|
||||
});
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Auth.Models.Api.Response;
|
||||
|
||||
@ -19,6 +20,8 @@ public class DeviceAuthRequestResponseModel : ResponseModel
|
||||
Identifier = deviceAuthDetails.Identifier,
|
||||
CreationDate = deviceAuthDetails.CreationDate,
|
||||
IsTrusted = deviceAuthDetails.IsTrusted,
|
||||
EncryptedPublicKey = deviceAuthDetails.EncryptedPublicKey,
|
||||
EncryptedUserKey = deviceAuthDetails.EncryptedUserKey
|
||||
};
|
||||
|
||||
if (deviceAuthDetails.AuthRequestId != null && deviceAuthDetails.AuthRequestCreatedAt != null)
|
||||
@ -39,6 +42,12 @@ public class DeviceAuthRequestResponseModel : ResponseModel
|
||||
public string Identifier { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
public bool IsTrusted { get; set; }
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(2000)]
|
||||
public string EncryptedUserKey { get; set; }
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(2000)]
|
||||
public string EncryptedPublicKey { get; set; }
|
||||
|
||||
public PendingAuthRequest DevicePendingAuthRequest { get; set; }
|
||||
|
||||
|
@ -29,6 +29,8 @@ public class DeviceAuthDetails : Device
|
||||
Identifier = device.Identifier;
|
||||
CreationDate = device.CreationDate;
|
||||
IsTrusted = device.IsTrusted();
|
||||
EncryptedPublicKey = device.EncryptedPublicKey;
|
||||
EncryptedUserKey = device.EncryptedUserKey;
|
||||
AuthRequestId = authRequestId;
|
||||
AuthRequestCreatedAt = authRequestCreationDate;
|
||||
}
|
||||
@ -74,6 +76,8 @@ public class DeviceAuthDetails : Device
|
||||
EncryptedPrivateKey = encryptedPrivateKey,
|
||||
Active = active
|
||||
}.IsTrusted();
|
||||
EncryptedPublicKey = encryptedPublicKey;
|
||||
EncryptedUserKey = encryptedUserKey;
|
||||
AuthRequestId = authRequestId != Guid.Empty ? authRequestId : null;
|
||||
AuthRequestCreatedAt =
|
||||
authRequestCreationDate != DateTime.MinValue ? authRequestCreationDate : null;
|
||||
|
@ -8,6 +8,7 @@ public interface IRegisterUserCommand
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new user, sends a welcome email, and raises the signup reference event.
|
||||
/// This method is used for JIT of organization Users.
|
||||
/// </summary>
|
||||
/// <param name="user">The <see cref="User"/> to create</param>
|
||||
/// <returns><see cref="IdentityResult"/></returns>
|
||||
|
@ -46,6 +46,7 @@ public static class StripeConstants
|
||||
|
||||
public static class MetadataKeys
|
||||
{
|
||||
public const string InvoiceApproved = "invoice_approved";
|
||||
public const string OrganizationId = "organizationId";
|
||||
public const string ProviderId = "providerId";
|
||||
public const string UserId = "userId";
|
||||
|
@ -27,4 +27,8 @@ public static class CustomerExtensions
|
||||
{
|
||||
return customer != null ? customer.Balance / 100M : default;
|
||||
}
|
||||
|
||||
public static bool ApprovedToPayByInvoice(this Customer customer)
|
||||
=> customer.Metadata.TryGetValue(StripeConstants.MetadataKeys.InvoiceApproved, out var value) &&
|
||||
int.TryParse(value, out var invoiceApproved) && invoiceApproved == 1;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Billing.Models.Api.Requests.Organizations;
|
||||
|
||||
@ -20,6 +21,8 @@ public class OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
public PlanType Plan { get; set; }
|
||||
|
||||
public PlanSponsorshipType? SponsoredPlan { get; set; }
|
||||
|
||||
[Range(0, int.MaxValue)]
|
||||
public int Seats { get; set; }
|
||||
|
||||
|
@ -91,9 +91,20 @@ public class OrganizationBillingService(
|
||||
|
||||
var subscription = await subscriberService.GetSubscription(organization);
|
||||
|
||||
if (customer == null || subscription == null)
|
||||
{
|
||||
return OrganizationMetadata.Default with
|
||||
{
|
||||
IsEligibleForSelfHost = isEligibleForSelfHost,
|
||||
IsManaged = isManaged
|
||||
};
|
||||
}
|
||||
|
||||
var isOnSecretsManagerStandalone = await IsOnSecretsManagerStandalone(organization, customer, subscription);
|
||||
|
||||
var invoice = await stripeAdapter.InvoiceGetAsync(subscription.LatestInvoiceId, new InvoiceGetOptions());
|
||||
var invoice = !string.IsNullOrEmpty(subscription.LatestInvoiceId)
|
||||
? await stripeAdapter.InvoiceGetAsync(subscription.LatestInvoiceId, new InvoiceGetOptions())
|
||||
: null;
|
||||
|
||||
return new OrganizationMetadata(
|
||||
isEligibleForSelfHost,
|
||||
|
@ -112,7 +112,6 @@ public static class FeatureFlagKeys
|
||||
/* Auth Team */
|
||||
public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence";
|
||||
public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence";
|
||||
public const string DuoRedirect = "duo-redirect";
|
||||
public const string EmailVerification = "email-verification";
|
||||
public const string DeviceTrustLogging = "pm-8285-device-trust-logging";
|
||||
public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token";
|
||||
@ -141,6 +140,7 @@ public static class FeatureFlagKeys
|
||||
/* Billing Team */
|
||||
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
||||
public const string TrialPayment = "PM-8163-trial-payment";
|
||||
public const string PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships";
|
||||
public const string UsePricingService = "use-pricing-service";
|
||||
public const string P15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal";
|
||||
public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features";
|
||||
@ -148,6 +148,7 @@ public static class FeatureFlagKeys
|
||||
public const string PM19147_AutomaticTaxImprovements = "pm-19147-automatic-tax-improvements";
|
||||
public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates";
|
||||
public const string PM18770_EnableOrganizationBusinessUnitConversion = "pm-18770-enable-organization-business-unit-conversion";
|
||||
public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically";
|
||||
|
||||
/* Key Management Team */
|
||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||
@ -156,6 +157,8 @@ public static class FeatureFlagKeys
|
||||
public const string Argon2Default = "argon2-default";
|
||||
public const string UserkeyRotationV2 = "userkey-rotation-v2";
|
||||
public const string SSHKeyItemVaultItem = "ssh-key-vault-item";
|
||||
public const string UserSdkForDecryption = "use-sdk-for-decryption";
|
||||
public const string PM17987_BlockType0 = "pm-17987-block-type-0";
|
||||
|
||||
/* Mobile Team */
|
||||
public const string NativeCarouselFlow = "native-carousel-flow";
|
||||
@ -172,6 +175,7 @@ public static class FeatureFlagKeys
|
||||
public const string PM3553_MobileSimpleLoginSelfHostAlias = "simple-login-self-host-alias";
|
||||
public const string EnablePMFlightRecorder = "enable-pm-flight-recorder";
|
||||
public const string MobileErrorReporting = "mobile-error-reporting";
|
||||
public const string AndroidChromeAutofill = "android-chrome-autofill";
|
||||
|
||||
/* Platform Team */
|
||||
public const string PersistPopupView = "persist-popup-view";
|
||||
@ -185,8 +189,6 @@ public static class FeatureFlagKeys
|
||||
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
|
||||
public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications";
|
||||
public const string DesktopSendUIRefresh = "desktop-send-ui-refresh";
|
||||
public const string ExportAttachments = "export-attachments";
|
||||
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
||||
|
||||
/* Vault Team */
|
||||
public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge";
|
||||
@ -198,6 +200,8 @@ public static class FeatureFlagKeys
|
||||
public const string SecurityTasks = "security-tasks";
|
||||
public const string CipherKeyEncryption = "cipher-key-encryption";
|
||||
public const string DesktopCipherForms = "pm-18520-desktop-cipher-forms";
|
||||
public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk";
|
||||
public const string EndUserNotifications = "pm-10609-end-user-notifications";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
{
|
||||
@ -210,9 +214,6 @@ public static class FeatureFlagKeys
|
||||
public static Dictionary<string, string> GetLocalOverrideFlagValues()
|
||||
{
|
||||
// place overriding values when needed locally (offline), or return null
|
||||
return new Dictionary<string, string>()
|
||||
{
|
||||
{ DuoRedirect, "true" },
|
||||
};
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,7 @@
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging.File" Version="3.0.0" />
|
||||
<PackageReference Include="Sentry.Serilog" Version="5.0.0" />
|
||||
<PackageReference Include="Duende.IdentityServer" Version="7.1.0" />
|
||||
<PackageReference Include="Duende.IdentityServer" Version="7.0.8" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="4.0.0" />
|
||||
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
|
||||
|
@ -20,6 +20,8 @@ public class OrganizationSponsorship : ITableObject<Guid>
|
||||
public DateTime? LastSyncDate { get; set; }
|
||||
public DateTime? ValidUntil { get; set; }
|
||||
public bool ToDelete { get; set; }
|
||||
public bool IsAdminInitiated { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
|
@ -22,4 +22,21 @@ public static class Claims
|
||||
|
||||
// General
|
||||
public const string Type = "type";
|
||||
|
||||
// Organization custom permissions
|
||||
public static class CustomPermissions
|
||||
{
|
||||
public const string AccessEventLogs = "accesseventlogs";
|
||||
public const string AccessImportExport = "accessimportexport";
|
||||
public const string AccessReports = "accessreports";
|
||||
public const string CreateNewCollections = "createnewcollections";
|
||||
public const string EditAnyCollection = "editanycollection";
|
||||
public const string DeleteAnyCollection = "deleteanycollection";
|
||||
public const string ManageGroups = "managegroups";
|
||||
public const string ManagePolicies = "managepolicies";
|
||||
public const string ManageSso = "managesso";
|
||||
public const string ManageUsers = "manageusers";
|
||||
public const string ManageResetPassword = "manageresetpassword";
|
||||
public const string ManageScim = "managescim";
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,8 @@ public class OrganizationSponsorshipData
|
||||
LastSyncDate = sponsorship.LastSyncDate;
|
||||
ValidUntil = sponsorship.ValidUntil;
|
||||
ToDelete = sponsorship.ToDelete;
|
||||
IsAdminInitiated = sponsorship.IsAdminInitiated;
|
||||
Notes = sponsorship.Notes;
|
||||
}
|
||||
public Guid SponsoringOrganizationUserId { get; set; }
|
||||
public Guid? SponsoredOrganizationId { get; set; }
|
||||
@ -25,6 +27,8 @@ public class OrganizationSponsorshipData
|
||||
public DateTime? LastSyncDate { get; set; }
|
||||
public DateTime? ValidUntil { get; set; }
|
||||
public bool ToDelete { get; set; }
|
||||
public bool IsAdminInitiated { get; set; }
|
||||
public string Notes { get; set; }
|
||||
|
||||
public bool CloudSponsorshipRemoved { get; set; }
|
||||
}
|
||||
|
@ -32,20 +32,22 @@ public class NotificationHubPushNotificationService : IPushNotificationService
|
||||
private readonly INotificationHubPool _notificationHubPool;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public NotificationHubPushNotificationService(
|
||||
IInstallationDeviceRepository installationDeviceRepository,
|
||||
INotificationHubPool notificationHubPool,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<NotificationHubPushNotificationService> logger,
|
||||
IGlobalSettings globalSettings)
|
||||
IGlobalSettings globalSettings,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_installationDeviceRepository = installationDeviceRepository;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_notificationHubPool = notificationHubPool;
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
|
||||
_timeProvider = timeProvider;
|
||||
if (globalSettings.Installation.Id == Guid.Empty)
|
||||
{
|
||||
logger.LogWarning("Installation ID is not set. Push notifications for installations will not work.");
|
||||
@ -152,7 +154,7 @@ public class NotificationHubPushNotificationService : IPushNotificationService
|
||||
|
||||
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
|
||||
{
|
||||
var message = new UserPushNotification { UserId = userId, Date = DateTime.UtcNow };
|
||||
var message = new UserPushNotification { UserId = userId, Date = _timeProvider.GetUtcNow().UtcDateTime };
|
||||
|
||||
await SendPayloadToUserAsync(userId, type, message, excludeCurrentContext);
|
||||
}
|
||||
|
@ -112,6 +112,13 @@ public class ValidateSponsorshipCommand : CancelSponsorshipCommand, IValidateSpo
|
||||
return false;
|
||||
}
|
||||
|
||||
if (existingSponsorship.IsAdminInitiated && !sponsoringOrganization.UseAdminSponsoredFamilies)
|
||||
{
|
||||
_logger.LogWarning("Admin initiated sponsorship for sponsored Organization {SponsoredOrganizationId} is not allowed because sponsoring organization does not have UseAdminSponsoredFamilies enabled", sponsoredOrganizationId);
|
||||
await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship);
|
||||
return false;
|
||||
}
|
||||
|
||||
var sponsoringOrgProductTier = sponsoringOrganization.PlanType.GetProductTier();
|
||||
|
||||
if (sponsoredPlan.SponsoringProductTierType != sponsoringOrgProductTier)
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -10,29 +11,24 @@ using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;
|
||||
|
||||
public class CreateSponsorshipCommand : ICreateSponsorshipCommand
|
||||
public class CreateSponsorshipCommand(
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
|
||||
IUserService userService) : ICreateSponsorshipCommand
|
||||
{
|
||||
private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public CreateSponsorshipCommand(IOrganizationSponsorshipRepository organizationSponsorshipRepository,
|
||||
IUserService userService)
|
||||
public async Task<OrganizationSponsorship> CreateSponsorshipAsync(Organization sponsoringOrganization,
|
||||
OrganizationUser sponsoringMember, PlanSponsorshipType sponsorshipType, string sponsoredEmail,
|
||||
string friendlyName, string notes)
|
||||
{
|
||||
_organizationSponsorshipRepository = organizationSponsorshipRepository;
|
||||
_userService = userService;
|
||||
}
|
||||
var sponsoringUser = await userService.GetUserByIdAsync(sponsoringMember.UserId!.Value);
|
||||
|
||||
public async Task<OrganizationSponsorship> CreateSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser,
|
||||
PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName)
|
||||
{
|
||||
var sponsoringUser = await _userService.GetUserByIdAsync(sponsoringOrgUser.UserId.Value);
|
||||
if (sponsoringUser == null || string.Equals(sponsoringUser.Email, sponsoredEmail, System.StringComparison.InvariantCultureIgnoreCase))
|
||||
if (sponsoringUser == null || string.Equals(sponsoringUser.Email, sponsoredEmail, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
throw new BadRequestException("Cannot offer a Families Organization Sponsorship to yourself. Choose a different email.");
|
||||
}
|
||||
|
||||
var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(sponsorshipType)?.SponsoringProductTierType;
|
||||
var sponsoringOrgProductTier = sponsoringOrg.PlanType.GetProductTier();
|
||||
var sponsoringOrgProductTier = sponsoringOrganization.PlanType.GetProductTier();
|
||||
|
||||
if (requiredSponsoringProductType == null ||
|
||||
sponsoringOrgProductTier != requiredSponsoringProductType.Value)
|
||||
@ -40,26 +36,24 @@ public class CreateSponsorshipCommand : ICreateSponsorshipCommand
|
||||
throw new BadRequestException("Specified Organization cannot sponsor other organizations.");
|
||||
}
|
||||
|
||||
if (sponsoringOrgUser == null || sponsoringOrgUser.Status != OrganizationUserStatusType.Confirmed)
|
||||
if (sponsoringMember.Status != OrganizationUserStatusType.Confirmed)
|
||||
{
|
||||
throw new BadRequestException("Only confirmed users can sponsor other organizations.");
|
||||
}
|
||||
|
||||
var existingOrgSponsorship = await _organizationSponsorshipRepository
|
||||
.GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id);
|
||||
var existingOrgSponsorship = await organizationSponsorshipRepository
|
||||
.GetBySponsoringOrganizationUserIdAsync(sponsoringMember.Id);
|
||||
if (existingOrgSponsorship?.SponsoredOrganizationId != null)
|
||||
{
|
||||
throw new BadRequestException("Can only sponsor one organization per Organization User.");
|
||||
}
|
||||
|
||||
var sponsorship = new OrganizationSponsorship
|
||||
{
|
||||
SponsoringOrganizationId = sponsoringOrg.Id,
|
||||
SponsoringOrganizationUserId = sponsoringOrgUser.Id,
|
||||
FriendlyName = friendlyName,
|
||||
OfferedToEmail = sponsoredEmail,
|
||||
PlanSponsorshipType = sponsorshipType,
|
||||
};
|
||||
var sponsorship = new OrganizationSponsorship();
|
||||
sponsorship.SponsoringOrganizationId = sponsoringOrganization.Id;
|
||||
sponsorship.SponsoringOrganizationUserId = sponsoringMember.Id;
|
||||
sponsorship.FriendlyName = friendlyName;
|
||||
sponsorship.OfferedToEmail = sponsoredEmail;
|
||||
sponsorship.PlanSponsorshipType = sponsorshipType;
|
||||
|
||||
if (existingOrgSponsorship != null)
|
||||
{
|
||||
@ -67,16 +61,42 @@ public class CreateSponsorshipCommand : ICreateSponsorshipCommand
|
||||
sponsorship.Id = existingOrgSponsorship.Id;
|
||||
}
|
||||
|
||||
var isAdminInitiated = false;
|
||||
if (currentContext.UserId != sponsoringMember.UserId)
|
||||
{
|
||||
var organization = currentContext.Organizations.First(x => x.Id == sponsoringOrganization.Id);
|
||||
OrganizationUserType[] allowedUserTypes =
|
||||
[
|
||||
OrganizationUserType.Admin,
|
||||
OrganizationUserType.Owner
|
||||
];
|
||||
|
||||
if (!organization.Permissions.ManageUsers && allowedUserTypes.All(x => x != organization.Type))
|
||||
{
|
||||
throw new UnauthorizedAccessException("You do not have permissions to send sponsorships on behalf of the organization.");
|
||||
}
|
||||
|
||||
if (!sponsoringOrganization.UseAdminSponsoredFamilies)
|
||||
{
|
||||
throw new BadRequestException("Sponsoring organization cannot sponsor other Family organizations.");
|
||||
}
|
||||
|
||||
isAdminInitiated = true;
|
||||
}
|
||||
|
||||
sponsorship.IsAdminInitiated = isAdminInitiated;
|
||||
sponsorship.Notes = notes;
|
||||
|
||||
try
|
||||
{
|
||||
await _organizationSponsorshipRepository.UpsertAsync(sponsorship);
|
||||
await organizationSponsorshipRepository.UpsertAsync(sponsorship);
|
||||
return sponsorship;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (sponsorship.Id != default)
|
||||
if (sponsorship.Id != Guid.Empty)
|
||||
{
|
||||
await _organizationSponsorshipRepository.DeleteAsync(sponsorship);
|
||||
await organizationSponsorshipRepository.DeleteAsync(sponsorship);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
|
@ -7,5 +7,5 @@ namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnte
|
||||
public interface ICreateSponsorshipCommand
|
||||
{
|
||||
Task<OrganizationSponsorship> CreateSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser,
|
||||
PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName);
|
||||
PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName, string notes);
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
namespace Bit.Core.Platform.Installations;
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Platform.Installations;
|
||||
|
||||
/// <summary>
|
||||
/// Command interface responsible for updating data on an `Installation`
|
||||
|
@ -1,4 +1,6 @@
|
||||
namespace Bit.Core.Platform.Installations;
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Platform.Installations;
|
||||
|
||||
/// <summary>
|
||||
/// Commands responsible for updating an installation from
|
||||
|
@ -1,4 +1,6 @@
|
||||
namespace Bit.Core.Platform.Installations;
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Platform.Installations;
|
||||
|
||||
/// <summary>
|
||||
/// Queries responsible for fetching an installation from
|
||||
@ -19,7 +21,7 @@ public class GetInstallationQuery : IGetInstallationQuery
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IGetInstallationQuery.GetByIdAsync"/>
|
||||
public async Task<Installation> GetByIdAsync(Guid installationId)
|
||||
public async Task<Installation?> GetByIdAsync(Guid installationId)
|
||||
{
|
||||
if (installationId == default(Guid))
|
||||
{
|
||||
|
@ -1,4 +1,6 @@
|
||||
namespace Bit.Core.Platform.Installations;
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Platform.Installations;
|
||||
|
||||
/// <summary>
|
||||
/// Query interface responsible for fetching an installation from
|
||||
@ -16,5 +18,5 @@ public interface IGetInstallationQuery
|
||||
/// <param name="installationId">The GUID id of the installation.</param>
|
||||
/// <returns>A task containing an `Installation`.</returns>
|
||||
/// <seealso cref="T:Bit.Core.Platform.Installations.Repositories.IInstallationRepository"/>
|
||||
Task<Installation> GetByIdAsync(Guid installationId);
|
||||
Task<Installation?> GetByIdAsync(Guid installationId);
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
using Bit.Core.Platform.Installations;
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.Platform.Installations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.Platform;
|
||||
|
@ -22,17 +22,19 @@ public class AzureQueuePushNotificationService : IPushNotificationService
|
||||
private readonly QueueClient _queueClient;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public AzureQueuePushNotificationService(
|
||||
[FromKeyedServices("notifications")] QueueClient queueClient,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IGlobalSettings globalSettings,
|
||||
ILogger<AzureQueuePushNotificationService> logger)
|
||||
ILogger<AzureQueuePushNotificationService> logger,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_queueClient = queueClient;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_globalSettings = globalSettings;
|
||||
|
||||
_timeProvider = timeProvider;
|
||||
if (globalSettings.Installation.Id == Guid.Empty)
|
||||
{
|
||||
logger.LogWarning("Installation ID is not set. Push notifications for installations will not work.");
|
||||
@ -140,7 +142,7 @@ public class AzureQueuePushNotificationService : IPushNotificationService
|
||||
|
||||
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
|
||||
{
|
||||
var message = new UserPushNotification { UserId = userId, Date = DateTime.UtcNow };
|
||||
var message = new UserPushNotification { UserId = userId, Date = _timeProvider.GetUtcNow().UtcDateTime };
|
||||
|
||||
await SendMessageAsync(type, message, excludeCurrentContext);
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
using Bit.Core.Enums;
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.NotificationHub;
|
||||
|
||||
namespace Bit.Core.Platform.Push;
|
||||
|
@ -1,4 +1,6 @@
|
||||
using Bit.Core.Enums;
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.NotificationHub;
|
||||
|
||||
namespace Bit.Core.Platform.Push.Internal;
|
||||
|
@ -24,12 +24,14 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
|
||||
{
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public NotificationsApiPushNotificationService(
|
||||
IHttpClientFactory httpFactory,
|
||||
GlobalSettings globalSettings,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<NotificationsApiPushNotificationService> logger)
|
||||
ILogger<NotificationsApiPushNotificationService> logger,
|
||||
TimeProvider timeProvider)
|
||||
: base(
|
||||
httpFactory,
|
||||
globalSettings.BaseServiceUri.InternalNotifications,
|
||||
@ -41,6 +43,7 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
|
||||
{
|
||||
_globalSettings = globalSettings;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_timeProvider = timeProvider;
|
||||
}
|
||||
|
||||
public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||
@ -148,7 +151,7 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
|
||||
var message = new UserPushNotification
|
||||
{
|
||||
UserId = userId,
|
||||
Date = DateTime.UtcNow
|
||||
Date = _timeProvider.GetUtcNow().UtcDateTime,
|
||||
};
|
||||
|
||||
await SendMessageAsync(type, message, excludeCurrentContext);
|
||||
|
@ -27,13 +27,15 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
|
||||
private readonly IDeviceRepository _deviceRepository;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public RelayPushNotificationService(
|
||||
IHttpClientFactory httpFactory,
|
||||
IDeviceRepository deviceRepository,
|
||||
GlobalSettings globalSettings,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<RelayPushNotificationService> logger)
|
||||
ILogger<RelayPushNotificationService> logger,
|
||||
TimeProvider timeProvider)
|
||||
: base(
|
||||
httpFactory,
|
||||
globalSettings.PushRelayBaseUri,
|
||||
@ -46,6 +48,7 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
|
||||
_deviceRepository = deviceRepository;
|
||||
_globalSettings = globalSettings;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_timeProvider = timeProvider;
|
||||
}
|
||||
|
||||
public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||
@ -147,7 +150,7 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
|
||||
|
||||
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
|
||||
{
|
||||
var message = new UserPushNotification { UserId = userId, Date = DateTime.UtcNow };
|
||||
var message = new UserPushNotification { UserId = userId, Date = _timeProvider.GetUtcNow().UtcDateTime };
|
||||
|
||||
await SendPayloadToUserAsync(userId, type, message, excludeCurrentContext);
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
using Bit.Core.Enums;
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.NotificationHub;
|
||||
|
@ -48,7 +48,7 @@ public interface ICollectionRepository : IRepository<Collection, Guid>
|
||||
Task<CollectionAdminDetails?> GetByIdWithPermissionsAsync(Guid collectionId, Guid? userId, bool includeAccessRelationships);
|
||||
|
||||
Task CreateAsync(Collection obj, IEnumerable<CollectionAccessSelection>? groups, IEnumerable<CollectionAccessSelection>? users);
|
||||
Task ReplaceAsync(Collection obj, IEnumerable<CollectionAccessSelection> groups, IEnumerable<CollectionAccessSelection> users);
|
||||
Task ReplaceAsync(Collection obj, IEnumerable<CollectionAccessSelection>? groups, IEnumerable<CollectionAccessSelection>? users);
|
||||
Task DeleteUserAsync(Guid collectionId, Guid organizationUserId);
|
||||
Task UpdateUsersAsync(Guid id, IEnumerable<CollectionAccessSelection> users);
|
||||
Task<ICollection<CollectionAccessSelection>> GetManyUsersByIdAsync(Guid id);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user