mirror of
https://github.com/bitwarden/server.git
synced 2024-12-01 13:43:23 +01:00
4c77f993e5
* [AC-1423] Add AddonProduct and BitwardenProduct properties to BillingSubscriptionItem (#3037) * [AC-1423] Add AddonProduct and BitwardenProduct properties to BillingSubscriptionItem - Add a helper method to determine the appropriate addon type based on the subscription items StripeId * [AC-1423] Add helper to StaticStore.cs to find a Plan by StripePlanId * [AC-1423] Use the helper method to set SubscriptionInfo.BitwardenProduct * Add SecretsManagerBilling feature flag to Constants * [AC 1409] Secrets Manager Subscription Stripe Integration (#3019) * Adding the Secret manager to the Plan List * Adding the unit test for the StaticStoreTests class * Fix whitespace formatting * Fix whitespace formatting * Price update * Resolving the PR comments * Resolving PR comments * Fixing the whitespace * only password manager plans are return for now * format whitespace * Resolve the test issue * Fixing the failing test * Refactoring the Plan separation * add a unit test for SingleOrDefault * Fix the whitespace format * Separate the PM and SM plans * Fixing the whitespace * Remove unnecessary directive * Fix imports ordering * Fix imports ordering * Resolve imports ordering * Fixing imports ordering * Fix response model, add MaxProjects * Fix filename * Fix format * Fix: seat price should match annual/monthly * Fix service account annual pricing * Changes for secret manager signup and upgradeplan * Changes for secrets manager signup and upgrade * refactoring the code * Format whitespace * remove unnecessary using directive * Resolve the PR comment on Subscription creation * Resolve PR comment * Add password manager to the error message * Add UseSecretsManager to the event log * Resolve PR comment on plan validation * Resolving pr comments for service account count * Resolving pr comments for service account count * Resolve the pr comments * Remove the store procedure that is no-longer needed * Rename a property properly * Resolving the PR comment * Resolve PR comments * Resolving PR comments * Resolving the Pr comments * Resolving some PR comments * Resolving the PR comments * Resolving the build identity build * Add additional Validation * Resolve the Lint issues * remove unnecessary using directive * Remove the white spaces * Adding unit test for the stripe payment * Remove the incomplete test * Fixing the failing test * Fix the failing test * Fix the fail test on organization service * Fix the failing unit test * Fix the whitespace format * Fix the failing test * Fix the whitespace format * resolve pr comments * Fix the lint message * Resolve the PR comments * resolve pr comments * Resolve pr comments * Resolve the pr comments * remove unused code * Added for sm validation test * Fix the whitespace format issues --------- Co-authored-by: Thomas Rittson <trittson@bitwarden.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> * SM-802: Add SecretsManagerBetaColumn SQL migration and Org table update * SM-802: Run EF Migrations for SecretsManagerBeta * SM-802: Update the two Org procs and View, and move data migration to a separate file * SM-802: Add missing comma to Organization_Create * [AC-1418] Add missing SecretsManagerPlan property to OrganizationResponseModel (#3055) * SM-802: Remove extra GO statement from data migration script * [AC 1460] Update Stripe Configuration (#3070) * change the stripeseat id * change service accountId to align with new product * make all the Id name for consistent * SM-802: Add SecretsManagerBeta to OrganizationResponseModel * SM-802: Move SecretsManagerBeta from OrganizationResponseModel to OrganizationSubscriptionResponseModel. Use sp_refreshview instead of sp_refreshsqlmodule in the migration script. * SM-802: Remove OrganizationUserOrganizationDetailsView.sql changes * [AC 1410] Secrets Manager subscription adjustment back-end changes (#3036) * Create UpgradeSecretsManagerSubscription command --------- Co-authored-by: Thomas Rittson <trittson@bitwarden.com> * SM-802: Remove SecretsManagerBetaColumn migration * SM-802: Add SecretsManagerBetaColumn migration * SM-802: Remove OrganizationUserOrganizationDetailsView update * [AC-1495] Extract UpgradePlanAsync into a command (#3081) * This is a pure lift & shift with no refactors * Only register subscription commands in Api --------- Co-authored-by: cyprain-okeke <cokeke@bitwarden.com> * [AC-1503] Fix Stripe integration on organization upgrade (#3084) * Fix SM parameters not being passed to Stripe * Fix flaky test * Fix error message * [AC-1504] Allow SM max autoscale limits to be disabled (#3085) * [AC-1488] Changed SM Signup and Upgrade paths to set SmServiceAccounts to include the plan BaseServiceAccount (#3086) * [AC-1510] Enable access to Secrets Manager to Organization owner for new Subscription (#3089) * Revert changes to ReferenceEvent code (#3091) * Revert changes to ReferenceEvent code This will be done in AC-1481 * Revert ReferenceEventType change * Move NoopServiceAccountRepository to SM and update namespace * [AC-1462] Add secrets manager service accounts autoscaling commands (#3059) * Adding the Secret manager to the Plan List * Adding the unit test for the StaticStoreTests class * Fix whitespace formatting * Fix whitespace formatting * Price update * Resolving the PR comments * Resolving PR comments * Fixing the whitespace * only password manager plans are return for now * format whitespace * Resolve the test issue * Fixing the failing test * Refactoring the Plan separation * add a unit test for SingleOrDefault * Fix the whitespace format * Separate the PM and SM plans * Fixing the whitespace * Remove unnecessary directive * Fix imports ordering * Fix imports ordering * Resolve imports ordering * Fixing imports ordering * Fix response model, add MaxProjects * Fix filename * Fix format * Fix: seat price should match annual/monthly * Fix service account annual pricing * Changes for secret manager signup and upgradeplan * Changes for secrets manager signup and upgrade * refactoring the code * Format whitespace * remove unnecessary using directive * Changes for subscription Update * Update the seatAdjustment and update * Resolve the PR comment on Subscription creation * Resolve PR comment * Add password manager to the error message * Add UseSecretsManager to the event log * Resolve PR comment on plan validation * Resolving pr comments for service account count * Resolving pr comments for service account count * Resolve the pr comments * Remove the store procedure that is no-longer needed * Add a new class for update subscription * Modify the Update subscription for sm * Add the missing property * Rename a property properly * Resolving the PR comment * Resolve PR comments * Resolving PR comments * Resolving the Pr comments * Resolving some PR comments * Resolving the PR comments * Resolving the build identity build * Add additional Validation * Resolve the Lint issues * remove unnecessary using directive * Remove the white spaces * Adding unit test for the stripe payment * Remove the incomplete test * Fixing the failing test * Fix the failing test * Fix the fail test on organization service * Fix the failing unit test * Fix the whitespace format * Fix the failing test * Fix the whitespace format * resolve pr comments * Fix the lint message * refactor the code * Fix the failing Test * adding a new endpoint * Remove the unwanted code * Changes for Command and Queries * changes for command and queries * Fix the Lint issues * Fix imports ordering * Resolve the PR comments * resolve pr comments * Resolve pr comments * Fix the failing test on adjustSeatscommandtests * Fix the failing test * Fix the whitespaces * resolve failing test * rename a property * Resolve the pr comments * refactoring the existing implementation * Resolve the whitespaces format issue * Resolve the pr comments * [AC-1462] Created IAvailableServiceAccountsQuery along its implementation and with unit tests * [AC-1462] Renamed ICountNewServiceAccountSlotsRequiredQuery * [AC-1462] Added IAutoscaleServiceAccountsCommand and implementation * Add more unit testing * fix the whitespaces issues * [AC-1462] Added unit tests for AutoscaleServiceAccountsCommand * Add more unit test * Remove unnecessary directive * Resolve some pr comments * Adding more unit test * adding more test * add more test * Resolving some pr comments * Resolving some pr comments * Resolving some pr comments * resolve some pr comments * Resolving pr comments * remove whitespaces * remove white spaces * Resolving pr comments * resolving pr comments and fixing white spaces * resolving the lint error * Run dotnet format * resolving the pr comments * Add a missing properties to plan response model * Add the email sender for sm seat and service acct * Add the email sender for sm seat and service acct * Fix the failing test after email sender changes * Add staticstorewrapper to properly test the plans * Add more test and validate the existing test * Fix the white spaces issues * Remove staticstorewrapper and fix the test * fix a null issue on autoscaling * Suggestion: do all seat calculations in update model * Resolve some pr comments * resolving some pr comments * Return value is unnecessary * Resolve the failing test * resolve pr comments * Resolve the pr comments * Resolving admin api failure and adding more test * Resolve the issue failing admin project * Fixing the failed test * Clarify naming and add comments * Clarify naming conventions * Dotnet format * Fix the failing dependency * remove similar test * [AC-1462] Rewrote AutoscaleServiceAccountsCommand to use UpdateSecretsManagerSubscriptionCommand which has the same logic * [AC-1462] Deleted IAutoscaleServiceAccountsCommand as the logic will be moved to UpdateSecretsManagerSubscriptionCommand * [AC-1462] Created method AdjustSecretsManagerServiceAccountsAsync * [AC-1462] Changed SecretsManagerSubscriptionUpdate to only be set by its constructor * [AC-1462] Added check to CountNewServiceAccountSlotsRequiredQuery and revised unit tests * [AC-1462] Revised logic for CountNewServiceAccountSlotsRequiredQuery and fixed unit tests * [AC-1462] Changed SecretsManagerSubscriptionUpdate to receive Organization as a parameter and fixed the unit tests * [AC-1462] Renamed IUpdateSecretsManagerSubscriptionCommand methods UpdateSubscriptionAsync and AdjustServiceAccountsAsync * [AC-1462] Rewrote unit test UpdateSubscriptionAsync_ValidInput_Passes * [AC-1462] Registered CountNewServiceAccountSlotsRequiredQuery for dependency injection * [AC-1462] Added parameter names to SecretsManagerSubscriptionUpdateRequestModel * [AC-1462] Updated SecretsManagerSubscriptionUpdate logic to handle null parameters. Revised the unit tests to test null values --------- Co-authored-by: cyprain-okeke <cokeke@bitwarden.com> Co-authored-by: Thomas Rittson <trittson@bitwarden.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> * Add UsePasswordManager to sync data (#3114) * [AC-1522] Fix service account check on upgrading (#3111) * Resolved the checkmarx issues * [AC-1521] Address checkmarx security feedback (#3124) * Reinstate target attribute but add noopener noreferrer * Update date on migration script * Remove unused constant * Revert "Remove unused constant" This reverts commit4fcb9da4d6
. This is required to make feature flags work on the client * [AC-1458] Add Endpoint And Service Logic for secrets manager to existing subscription (#3087) --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Thomas Rittson <trittson@bitwarden.com> * Remove duplicate migrations from incorrectly resolved merge * [AC-1468] Modified CountNewServiceAccountSlotsRequiredQuery to return zero if organization has SecretsManagerBeta == true (#3112) Co-authored-by: Thomas Rittson <trittson@bitwarden.com> * [Ac 1563] Unable to load billing and subscription related pages for non-enterprise organizations (#3138) * Resolve the failing family plan * resolve issues * Resolve code related pr comments * Resolve test related comments * Resolving or comments * [SM-809] Add service account slot limit check (#3093) * Add service account slot limit check * Add query to DI * [AC-1462] Registered CountNewServiceAccountSlotsRequiredQuery for dependency injection * remove duplicate DI entry * Update unit tests * Remove comment * Code review updates --------- Co-authored-by: cyprain-okeke <cokeke@bitwarden.com> Co-authored-by: Thomas Rittson <trittson@bitwarden.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Rui Tome <rtome@bitwarden.com> * [AC-1461] Secrets manager seat autoscaling (#3121) * Add autoscaling code to invite user, save user, and bulk enable SM flows * Add tests * Delete command for BulkEnableSecretsManager * circular dependency between OrganizationService and UpdateSecretsManagerSubscriptionCommand - fixed by temporarily duplicating ReplaceAndUpdateCache * Unresolvable dependencies in other services - fixed by temporarily registering noop services and moving around some DI code All should be resolved in PM-1880 * Refactor: improve the update object and use it to adjust values, remove excess interfaces on the command * Handle autoscaling-specific errors --------- Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> * Move bitwarden_license include reference into conditional block * [AC 1526]Show current SM seat and service account usage in Bitwarden Portal (#3142) * changes base on the tickets request * Code refactoring * Removed the unwanted method * Add implementation to the new method * Resolve some pr comments * resolve lint issue * resolve pr comments * add the new noop files * Add new noop file and resolve some pr comments * resolve pr comments * removed unused method * Fix prefill when changing plans or starting trials * [AC-1575] Don't overwrite existing org information when changing plans * [AC-1577] Prefill SM configuration section when starting trial * Clarify property names * Set SM subscription based on current usage * Fix label for Service Accounts field * Prevent subscribing to SM on an invalid plan * Edit comment * Set minimum seat count for SM * Don't use hardcoded plan values * Use plans directly in JS * Refactor to getPlan function * Remove unneeded namespace refs * Add GetPlansHelper to provide some typesafety * Use planType from form instead of model * Add @model specification * Add null coalescing * [AC-108] Updated PolicyService to use IApplicationCacheService to determine if an organization uses policies (cherry picked from commitb98b107c4b
) * [AC-108] Removed checking if Organization is enabled --------- Co-authored-by: Shane Melton <smelton@bitwarden.com> Co-authored-by: Thomas Rittson <trittson@bitwarden.com> Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Colton Hurst <colton@coltonhurst.com> Co-authored-by: cyprain-okeke <cokeke@bitwarden.com> Co-authored-by: Conner Turnbull <cturnbull@bitwarden.com> Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
659 lines
28 KiB
C#
659 lines
28 KiB
C#
using System.Text.Json;
|
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
|
using Bit.Core.Enums;
|
|
using Bit.Core.Repositories;
|
|
using Bit.Identity.IdentityServer;
|
|
using Bit.IntegrationTestCommon.Factories;
|
|
using Bit.Test.Common.AutoFixture.Attributes;
|
|
using Bit.Test.Common.Helpers;
|
|
using Microsoft.AspNetCore.TestHost;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Xunit;
|
|
|
|
namespace Bit.Identity.IntegrationTest.Endpoints;
|
|
|
|
public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
|
{
|
|
private const int SecondsInMinute = 60;
|
|
private const int MinutesInHour = 60;
|
|
private const int SecondsInHour = SecondsInMinute * MinutesInHour;
|
|
private readonly IdentityApplicationFactory _factory;
|
|
|
|
public IdentityServerTests(IdentityApplicationFactory factory)
|
|
{
|
|
_factory = factory;
|
|
ReinitializeDbForTests();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task WellKnownEndpoint_Success()
|
|
{
|
|
var context = await _factory.Server.GetAsync("/.well-known/openid-configuration");
|
|
|
|
using var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
|
var endpointRoot = body.RootElement;
|
|
|
|
// WARNING: Edits to this file should NOT just be made to "get the test to work" they should be made when intentional
|
|
// changes were made to this endpoint and proper testing will take place to ensure clients are backwards compatible
|
|
// or loss of functionality is properly noted.
|
|
await using var fs = File.OpenRead("openid-configuration.json");
|
|
using var knownConfiguration = await JsonSerializer.DeserializeAsync<JsonDocument>(fs);
|
|
var knownConfigurationRoot = knownConfiguration.RootElement;
|
|
|
|
AssertHelper.AssertEqualJson(endpointRoot, knownConfigurationRoot);
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task TokenEndpoint_GrantTypePassword_Success(string deviceId)
|
|
{
|
|
var username = "test+tokenpassword@email.com";
|
|
|
|
await _factory.RegisterAsync(new RegisterRequestModel
|
|
{
|
|
Email = username,
|
|
MasterPasswordHash = "master_password_hash"
|
|
});
|
|
|
|
var context = await PostLoginAsync(_factory.Server, username, deviceId, context => context.SetAuthEmail(username));
|
|
|
|
using var body = await AssertDefaultTokenBodyAsync(context);
|
|
var root = body.RootElement;
|
|
AssertRefreshTokenExists(root);
|
|
AssertHelper.AssertJsonProperty(root, "ForcePasswordReset", JsonValueKind.False);
|
|
AssertHelper.AssertJsonProperty(root, "ResetMasterPassword", JsonValueKind.False);
|
|
var kdf = AssertHelper.AssertJsonProperty(root, "Kdf", JsonValueKind.Number).GetInt32();
|
|
Assert.Equal(0, kdf);
|
|
var kdfIterations = AssertHelper.AssertJsonProperty(root, "KdfIterations", JsonValueKind.Number).GetInt32();
|
|
Assert.Equal(5000, kdfIterations);
|
|
AssertUserDecryptionOptions(root);
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task TokenEndpoint_GrantTypePassword_NoAuthEmailHeader_Fails(string deviceId)
|
|
{
|
|
var username = "test+noauthemailheader@email.com";
|
|
|
|
await _factory.RegisterAsync(new RegisterRequestModel
|
|
{
|
|
Email = username,
|
|
MasterPasswordHash = "master_password_hash",
|
|
});
|
|
|
|
var context = await PostLoginAsync(_factory.Server, username, deviceId, null);
|
|
|
|
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
|
|
|
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
|
var root = body.RootElement;
|
|
|
|
var error = AssertHelper.AssertJsonProperty(root, "error", JsonValueKind.String).GetString();
|
|
Assert.Equal("invalid_grant", error);
|
|
AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String);
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task TokenEndpoint_GrantTypePassword_InvalidBase64AuthEmailHeader_Fails(string deviceId)
|
|
{
|
|
var username = "test+badauthheader@email.com";
|
|
|
|
await _factory.RegisterAsync(new RegisterRequestModel
|
|
{
|
|
Email = username,
|
|
MasterPasswordHash = "master_password_hash",
|
|
});
|
|
|
|
var context = await PostLoginAsync(_factory.Server, username, deviceId, context => context.Request.Headers.Add("Auth-Email", "bad_value"));
|
|
|
|
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
|
|
|
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
|
var root = body.RootElement;
|
|
|
|
var error = AssertHelper.AssertJsonProperty(root, "error", JsonValueKind.String).GetString();
|
|
Assert.Equal("invalid_grant", error);
|
|
AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String);
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task TokenEndpoint_GrantTypePassword_WrongAuthEmailHeader_Fails(string deviceId)
|
|
{
|
|
var username = "test+badauthheader@email.com";
|
|
|
|
await _factory.RegisterAsync(new RegisterRequestModel
|
|
{
|
|
Email = username,
|
|
MasterPasswordHash = "master_password_hash",
|
|
});
|
|
|
|
var context = await PostLoginAsync(_factory.Server, username, deviceId, context => context.SetAuthEmail("bad_value"));
|
|
|
|
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
|
|
|
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
|
var root = body.RootElement;
|
|
|
|
var error = AssertHelper.AssertJsonProperty(root, "error", JsonValueKind.String).GetString();
|
|
Assert.Equal("invalid_grant", error);
|
|
AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String);
|
|
}
|
|
|
|
[Theory]
|
|
[BitAutoData(OrganizationUserType.Owner)]
|
|
[BitAutoData(OrganizationUserType.Admin)]
|
|
[BitAutoData(OrganizationUserType.User)]
|
|
[BitAutoData(OrganizationUserType.Manager)]
|
|
[BitAutoData(OrganizationUserType.Custom)]
|
|
public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyDisabled_WithEnforceSsoPolicyForAllUsersTrue_Success(OrganizationUserType organizationUserType, Guid organizationId, string deviceId, int generatedUsername)
|
|
{
|
|
var username = $"{generatedUsername}@example.com";
|
|
|
|
var server = _factory.WithWebHostBuilder(builder =>
|
|
{
|
|
builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "true");
|
|
}).Server;
|
|
|
|
await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel
|
|
{
|
|
Email = username,
|
|
MasterPasswordHash = "master_password_hash"
|
|
}));
|
|
|
|
await CreateOrganizationWithSsoPolicyAsync(organizationId, username, organizationUserType, ssoPolicyEnabled: false);
|
|
|
|
var context = await PostLoginAsync(server, username, deviceId, context => context.SetAuthEmail(username));
|
|
|
|
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
|
}
|
|
|
|
[Theory]
|
|
[BitAutoData(OrganizationUserType.Owner)]
|
|
[BitAutoData(OrganizationUserType.Admin)]
|
|
[BitAutoData(OrganizationUserType.User)]
|
|
[BitAutoData(OrganizationUserType.Manager)]
|
|
[BitAutoData(OrganizationUserType.Custom)]
|
|
public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyDisabled_WithEnforceSsoPolicyForAllUsersFalse_Success(OrganizationUserType organizationUserType, Guid organizationId, string deviceId, int generatedUsername)
|
|
{
|
|
var username = $"{generatedUsername}@example.com";
|
|
|
|
var server = _factory.WithWebHostBuilder(builder =>
|
|
{
|
|
builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "false");
|
|
}).Server;
|
|
|
|
await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel
|
|
{
|
|
Email = username,
|
|
MasterPasswordHash = "master_password_hash"
|
|
}));
|
|
|
|
await CreateOrganizationWithSsoPolicyAsync(organizationId, username, organizationUserType, ssoPolicyEnabled: false);
|
|
|
|
var context = await PostLoginAsync(server, username, deviceId, context => context.SetAuthEmail(username));
|
|
|
|
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
|
}
|
|
|
|
[Theory]
|
|
[BitAutoData(OrganizationUserType.Owner)]
|
|
[BitAutoData(OrganizationUserType.Admin)]
|
|
[BitAutoData(OrganizationUserType.User)]
|
|
[BitAutoData(OrganizationUserType.Manager)]
|
|
[BitAutoData(OrganizationUserType.Custom)]
|
|
public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersTrue_Throw(OrganizationUserType organizationUserType, Guid organizationId, string deviceId, int generatedUsername)
|
|
{
|
|
var username = $"{generatedUsername}@example.com";
|
|
|
|
var server = _factory.WithWebHostBuilder(builder =>
|
|
{
|
|
builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "true");
|
|
}).Server;
|
|
|
|
await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel
|
|
{
|
|
Email = username,
|
|
MasterPasswordHash = "master_password_hash"
|
|
}));
|
|
|
|
await CreateOrganizationWithSsoPolicyAsync(organizationId, username, organizationUserType, ssoPolicyEnabled: true);
|
|
|
|
var context = await PostLoginAsync(server, username, deviceId, context => context.SetAuthEmail(username));
|
|
|
|
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
|
await AssertRequiredSsoAuthenticationResponseAsync(context);
|
|
}
|
|
|
|
[Theory]
|
|
[BitAutoData(OrganizationUserType.Owner)]
|
|
[BitAutoData(OrganizationUserType.Admin)]
|
|
public async Task TokenEndpoint_GrantTypePassword_WithOwnerOrAdmin_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersFalse_Success(OrganizationUserType organizationUserType, Guid organizationId, string deviceId, int generatedUsername)
|
|
{
|
|
var username = $"{generatedUsername}@example.com";
|
|
|
|
var server = _factory.WithWebHostBuilder(builder =>
|
|
{
|
|
builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "false");
|
|
}).Server;
|
|
|
|
await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel
|
|
{
|
|
Email = username,
|
|
MasterPasswordHash = "master_password_hash"
|
|
}));
|
|
|
|
await CreateOrganizationWithSsoPolicyAsync(organizationId, username, organizationUserType, ssoPolicyEnabled: true);
|
|
|
|
var context = await PostLoginAsync(server, username, deviceId, context => context.SetAuthEmail(username));
|
|
|
|
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
|
}
|
|
|
|
[Theory]
|
|
[BitAutoData(OrganizationUserType.User)]
|
|
[BitAutoData(OrganizationUserType.Manager)]
|
|
[BitAutoData(OrganizationUserType.Custom)]
|
|
public async Task TokenEndpoint_GrantTypePassword_WithNonOwnerOrAdmin_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersFalse_Throws(OrganizationUserType organizationUserType, Guid organizationId, string deviceId, int generatedUsername)
|
|
{
|
|
var username = $"{generatedUsername}@example.com";
|
|
|
|
var server = _factory.WithWebHostBuilder(builder =>
|
|
{
|
|
builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "false");
|
|
}).Server;
|
|
|
|
await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel
|
|
{
|
|
Email = username,
|
|
MasterPasswordHash = "master_password_hash"
|
|
}));
|
|
|
|
await CreateOrganizationWithSsoPolicyAsync(organizationId, username, organizationUserType, ssoPolicyEnabled: true);
|
|
|
|
var context = await PostLoginAsync(server, username, deviceId, context => context.SetAuthEmail(username));
|
|
|
|
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
|
await AssertRequiredSsoAuthenticationResponseAsync(context);
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task TokenEndpoint_GrantTypeRefreshToken_Success(string deviceId)
|
|
{
|
|
var username = "test+tokenrefresh@email.com";
|
|
|
|
await _factory.RegisterAsync(new RegisterRequestModel
|
|
{
|
|
Email = username,
|
|
MasterPasswordHash = "master_password_hash",
|
|
});
|
|
|
|
var (_, refreshToken) = await _factory.TokenFromPasswordAsync(username, "master_password_hash", deviceId);
|
|
|
|
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
|
{
|
|
{ "grant_type", "refresh_token" },
|
|
{ "client_id", "web" },
|
|
{ "refresh_token", refreshToken },
|
|
}));
|
|
|
|
using var body = await AssertDefaultTokenBodyAsync(context);
|
|
AssertRefreshTokenExists(body.RootElement);
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task TokenEndpoint_GrantTypeClientCredentials_Success(string deviceId)
|
|
{
|
|
var username = "test+tokenclientcredentials@email.com";
|
|
|
|
await _factory.RegisterAsync(new RegisterRequestModel
|
|
{
|
|
Email = username,
|
|
MasterPasswordHash = "master_password_hash",
|
|
});
|
|
|
|
var database = _factory.GetDatabaseContext();
|
|
var user = await database.Users
|
|
.FirstAsync(u => u.Email == username);
|
|
|
|
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
|
{
|
|
{ "grant_type", "client_credentials" },
|
|
{ "client_id", $"user.{user.Id}" },
|
|
{ "client_secret", user.ApiKey },
|
|
{ "scope", "api" },
|
|
{ "DeviceIdentifier", deviceId },
|
|
{ "DeviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
|
{ "DeviceName", "firefox" },
|
|
}));
|
|
|
|
await AssertDefaultTokenBodyAsync(context, "api");
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task TokenEndpoint_GrantTypeClientCredentials_AsOrganization_Success(Bit.Core.Entities.Organization organization, Bit.Core.Entities.OrganizationApiKey organizationApiKey)
|
|
{
|
|
var orgRepo = _factory.Services.GetRequiredService<IOrganizationRepository>();
|
|
organization.Enabled = true;
|
|
organization.UseApi = true;
|
|
organization = await orgRepo.CreateAsync(organization);
|
|
organizationApiKey.OrganizationId = organization.Id;
|
|
organizationApiKey.Type = OrganizationApiKeyType.Default;
|
|
|
|
var orgApiKeyRepo = _factory.Services.GetRequiredService<IOrganizationApiKeyRepository>();
|
|
await orgApiKeyRepo.CreateAsync(organizationApiKey);
|
|
|
|
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
|
{
|
|
{ "grant_type", "client_credentials" },
|
|
{ "client_id", $"organization.{organization.Id}" },
|
|
{ "client_secret", organizationApiKey.ApiKey },
|
|
{ "scope", "api.organization" },
|
|
}));
|
|
|
|
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
|
|
|
await AssertDefaultTokenBodyAsync(context, "api.organization");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TokenEndpoint_GrantTypeClientCredentials_AsOrganization_BadOrgId_Fails()
|
|
{
|
|
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
|
{
|
|
{ "grant_type", "client_credentials" },
|
|
{ "client_id", "organization.bad_guid_zz&" },
|
|
{ "client_secret", "something" },
|
|
{ "scope", "api.organization" },
|
|
}));
|
|
|
|
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
|
|
|
var errorBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
|
var error = AssertHelper.AssertJsonProperty(errorBody.RootElement, "error", JsonValueKind.String).GetString();
|
|
Assert.Equal("invalid_client", error);
|
|
}
|
|
|
|
/// <summary>
|
|
/// This test currently does not test any code that is not covered by other tests but
|
|
/// it shows that we probably have some dead code in <see cref="ClientStore"/>
|
|
/// for installation, organization, and user they split on a <c>'.'</c> but have already checked that at least one
|
|
/// <c>'.'</c> exists in the <c>client_id</c> by checking it with <see cref="string.StartsWith(string)"/>
|
|
/// I believe that idParts.Length > 1 will ALWAYS return true
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task TokenEndpoint_GrantTypeClientCredentials_AsOrganization_NoIdPart_Fails()
|
|
{
|
|
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
|
{
|
|
{ "grant_type", "client_credentials" },
|
|
{ "client_id", "organization." },
|
|
{ "client_secret", "something" },
|
|
{ "scope", "api.organization" },
|
|
}));
|
|
|
|
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
|
|
|
var errorBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
|
var error = AssertHelper.AssertJsonProperty(errorBody.RootElement, "error", JsonValueKind.String).GetString();
|
|
Assert.Equal("invalid_client", error);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TokenEndpoint_GrantTypeClientCredentials_AsOrganization_OrgDoesNotExist_Fails()
|
|
{
|
|
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
|
{
|
|
{ "grant_type", "client_credentials" },
|
|
{ "client_id", $"organization.{Guid.NewGuid()}" },
|
|
{ "client_secret", "something" },
|
|
{ "scope", "api.organization" },
|
|
}));
|
|
|
|
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
|
|
|
var errorBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
|
var error = AssertHelper.AssertJsonProperty(errorBody.RootElement, "error", JsonValueKind.String).GetString();
|
|
Assert.Equal("invalid_client", error);
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task TokenEndpoint_GrantTypeClientCredentials_AsInstallation_InstallationExists_Succeeds(Bit.Core.Entities.Installation installation)
|
|
{
|
|
var installationRepo = _factory.Services.GetRequiredService<IInstallationRepository>();
|
|
installation = await installationRepo.CreateAsync(installation);
|
|
|
|
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
|
{
|
|
{ "grant_type", "client_credentials" },
|
|
{ "client_id", $"installation.{installation.Id}" },
|
|
{ "client_secret", installation.Key },
|
|
{ "scope", "api.push" },
|
|
}));
|
|
|
|
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
|
await AssertDefaultTokenBodyAsync(context, "api.push", 24 * SecondsInHour);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TokenEndpoint_GrantTypeClientCredentials_AsInstallation_InstallationDoesNotExist_Fails()
|
|
{
|
|
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
|
{
|
|
{ "grant_type", "client_credentials" },
|
|
{ "client_id", $"installation.{Guid.NewGuid()}" },
|
|
{ "client_secret", "something" },
|
|
{ "scope", "api.push" },
|
|
}));
|
|
|
|
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
|
|
|
var errorBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
|
var error = AssertHelper.AssertJsonProperty(errorBody.RootElement, "error", JsonValueKind.String).GetString();
|
|
Assert.Equal("invalid_client", error);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TokenEndpoint_GrantTypeClientCredentials_AsInstallation_BadInstallationId_Fails()
|
|
{
|
|
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
|
{
|
|
{ "grant_type", "client_credentials" },
|
|
{ "client_id", "organization.bad_guid_zz&" },
|
|
{ "client_secret", "something" },
|
|
{ "scope", "api.organization" },
|
|
}));
|
|
|
|
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
|
|
|
var errorBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
|
var error = AssertHelper.AssertJsonProperty(errorBody.RootElement, "error", JsonValueKind.String).GetString();
|
|
Assert.Equal("invalid_client", error);
|
|
}
|
|
|
|
/// <inheritdoc cref="TokenEndpoint_GrantTypeClientCredentials_AsOrganization_NoIdPart_Fails"/>
|
|
[Fact]
|
|
public async Task TokenEndpoint_GrantTypeClientCredentials_AsInstallation_NoIdPart_Fails()
|
|
{
|
|
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
|
{
|
|
{ "grant_type", "client_credentials" },
|
|
{ "client_id", "installation." },
|
|
{ "client_secret", "something" },
|
|
{ "scope", "api.push" },
|
|
}));
|
|
|
|
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
|
|
|
var errorBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
|
var error = AssertHelper.AssertJsonProperty(errorBody.RootElement, "error", JsonValueKind.String).GetString();
|
|
Assert.Equal("invalid_client", error);
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task TokenEndpoint_TooQuickInOneSecond_BlockRequest(string deviceId)
|
|
{
|
|
const int AmountInOneSecondAllowed = 10;
|
|
|
|
// The rule we are testing is 10 requests in 1 second
|
|
var username = "test+ratelimiting@email.com";
|
|
|
|
await _factory.RegisterAsync(new RegisterRequestModel
|
|
{
|
|
Email = username,
|
|
MasterPasswordHash = "master_password_hash",
|
|
});
|
|
|
|
var database = _factory.GetDatabaseContext();
|
|
var user = await database.Users
|
|
.FirstAsync(u => u.Email == username);
|
|
|
|
var tasks = new Task<HttpContext>[AmountInOneSecondAllowed + 1];
|
|
|
|
for (var i = 0; i < AmountInOneSecondAllowed + 1; i++)
|
|
{
|
|
// Queue all the amount of calls allowed plus 1
|
|
tasks[i] = MakeRequest();
|
|
}
|
|
|
|
var responses = (await Task.WhenAll(tasks)).ToList();
|
|
var blockResponses = responses.Where(c => c.Response.StatusCode == StatusCodes.Status429TooManyRequests);
|
|
|
|
Assert.True(blockResponses.Count() > 0);
|
|
|
|
Task<HttpContext> MakeRequest()
|
|
{
|
|
return _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
|
{
|
|
{ "scope", "api offline_access" },
|
|
{ "client_id", "web" },
|
|
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
|
{ "deviceIdentifier", deviceId },
|
|
{ "deviceName", "firefox" },
|
|
{ "grant_type", "password" },
|
|
{ "username", username },
|
|
{ "password", "master_password_hash" },
|
|
}), context => context.SetAuthEmail(username).SetIp("1.1.1.2"));
|
|
}
|
|
}
|
|
|
|
private async Task<HttpContext> PostLoginAsync(TestServer server, string username, string deviceId, Action<HttpContext> extraConfiguration)
|
|
{
|
|
return await server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
|
{
|
|
{ "scope", "api offline_access" },
|
|
{ "client_id", "web" },
|
|
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
|
{ "deviceIdentifier", deviceId },
|
|
{ "deviceName", "firefox" },
|
|
{ "grant_type", "password" },
|
|
{ "username", username },
|
|
{ "password", "master_password_hash" },
|
|
}), extraConfiguration);
|
|
}
|
|
|
|
private async Task CreateOrganizationWithSsoPolicyAsync(Guid organizationId, string username, OrganizationUserType organizationUserType, bool ssoPolicyEnabled)
|
|
{
|
|
var userRepository = _factory.Services.GetService<IUserRepository>();
|
|
var organizationRepository = _factory.Services.GetService<IOrganizationRepository>();
|
|
var organizationUserRepository = _factory.Services.GetService<IOrganizationUserRepository>();
|
|
var policyRepository = _factory.Services.GetService<IPolicyRepository>();
|
|
|
|
var organization = new Bit.Core.Entities.Organization { Id = organizationId, Enabled = true, UseSso = ssoPolicyEnabled, UsePolicies = true };
|
|
await organizationRepository.CreateAsync(organization);
|
|
|
|
var user = await userRepository.GetByEmailAsync(username);
|
|
var organizationUser = new Bit.Core.Entities.OrganizationUser
|
|
{
|
|
OrganizationId = organization.Id,
|
|
UserId = user.Id,
|
|
Status = OrganizationUserStatusType.Confirmed,
|
|
Type = organizationUserType
|
|
};
|
|
await organizationUserRepository.CreateAsync(organizationUser);
|
|
|
|
var ssoPolicy = new Bit.Core.Entities.Policy { OrganizationId = organization.Id, Type = PolicyType.RequireSso, Enabled = ssoPolicyEnabled };
|
|
await policyRepository.CreateAsync(ssoPolicy);
|
|
}
|
|
|
|
private static string DeviceTypeAsString(DeviceType deviceType)
|
|
{
|
|
return ((int)deviceType).ToString();
|
|
}
|
|
|
|
private static async Task<JsonDocument> AssertDefaultTokenBodyAsync(HttpContext httpContext, string expectedScope = "api offline_access", int expectedExpiresIn = SecondsInHour * 1)
|
|
{
|
|
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(httpContext);
|
|
var root = body.RootElement;
|
|
|
|
Assert.Equal(JsonValueKind.Object, root.ValueKind);
|
|
AssertAccessTokenExists(root);
|
|
AssertExpiresIn(root, expectedExpiresIn);
|
|
AssertTokenType(root);
|
|
AssertScope(root, expectedScope);
|
|
return body;
|
|
}
|
|
|
|
private static void AssertTokenType(JsonElement tokenResponse)
|
|
{
|
|
var tokenTypeProperty = AssertHelper.AssertJsonProperty(tokenResponse, "token_type", JsonValueKind.String).GetString();
|
|
Assert.Equal("Bearer", tokenTypeProperty);
|
|
}
|
|
|
|
private static int AssertExpiresIn(JsonElement tokenResponse, int expectedExpiresIn = 3600)
|
|
{
|
|
var expiresIn = AssertHelper.AssertJsonProperty(tokenResponse, "expires_in", JsonValueKind.Number).GetInt32();
|
|
Assert.Equal(expectedExpiresIn, expiresIn);
|
|
return expiresIn;
|
|
}
|
|
|
|
private static string AssertAccessTokenExists(JsonElement tokenResponse)
|
|
{
|
|
return AssertHelper.AssertJsonProperty(tokenResponse, "access_token", JsonValueKind.String).GetString();
|
|
}
|
|
|
|
private static string AssertRefreshTokenExists(JsonElement tokenResponse)
|
|
{
|
|
return AssertHelper.AssertJsonProperty(tokenResponse, "refresh_token", JsonValueKind.String).GetString();
|
|
}
|
|
|
|
private static string AssertScopeExists(JsonElement tokenResponse)
|
|
{
|
|
return AssertHelper.AssertJsonProperty(tokenResponse, "scope", JsonValueKind.String).GetString();
|
|
}
|
|
|
|
private static void AssertScope(JsonElement tokenResponse, string expectedScope)
|
|
{
|
|
var actualScope = AssertScopeExists(tokenResponse);
|
|
Assert.Equal(expectedScope, actualScope);
|
|
}
|
|
|
|
private static async Task AssertRequiredSsoAuthenticationResponseAsync(HttpContext context)
|
|
{
|
|
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
|
var root = body.RootElement;
|
|
|
|
var error = AssertHelper.AssertJsonProperty(root, "error", JsonValueKind.String).GetString();
|
|
Assert.Equal("invalid_grant", error);
|
|
var errorDescription = AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String).GetString();
|
|
Assert.StartsWith("sso authentication", errorDescription.ToLowerInvariant());
|
|
}
|
|
|
|
private static void AssertUserDecryptionOptions(JsonElement tokenResponse)
|
|
{
|
|
var userDecryptionOptions = AssertHelper.AssertJsonProperty(tokenResponse, "UserDecryptionOptions", JsonValueKind.Object)
|
|
.EnumerateObject();
|
|
|
|
Assert.Collection(userDecryptionOptions,
|
|
(prop) => { Assert.Equal("HasMasterPassword", prop.Name); Assert.Equal(JsonValueKind.True, prop.Value.ValueKind); },
|
|
(prop) => { Assert.Equal("Object", prop.Name); Assert.Equal("userDecryptionOptions", prop.Value.GetString()); });
|
|
}
|
|
|
|
private void ReinitializeDbForTests()
|
|
{
|
|
var databaseContext = _factory.GetDatabaseContext();
|
|
databaseContext.Policies.RemoveRange(databaseContext.Policies);
|
|
databaseContext.OrganizationUsers.RemoveRange(databaseContext.OrganizationUsers);
|
|
databaseContext.Organizations.RemoveRange(databaseContext.Organizations);
|
|
databaseContext.Users.RemoveRange(databaseContext.Users);
|
|
databaseContext.SaveChanges();
|
|
}
|
|
}
|