From 25a99919084d872b694ed4a0034f72a5040390d8 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Tue, 10 Nov 2020 15:15:29 -0500 Subject: [PATCH] Implement User-based API Keys (#981) * added column ApiKey to dbo.User * added dbo.User.ApiKey to User_Update * added dbo.User.ApiKey to User_Create * wrote migration script for implementing dbo.User.ApiKey * Added ApiKey prop to the User table model * Created AccountsController method for getting a user's API Key * Created AccountsController method for rotating a user API key * Added support to ApiClient for passed-through ClientSecrets when the request comes from the cli * Added a new conditional to ClientStore to account for user API keys * Wrote unit tests for new user API Key methods * Added a refresh of dbo.UserView to new migration script for ApiKey * Let client_credentials grants into the custom token logic * Cleanup for ApiKey auth in the CLI feature * Created user API key on registration * Removed uneeded code for user API keys * Changed a .Contains() to a .StartsWith() in ClientStore * Changed index that an array is searched on * Added more claims to the user apikey clients * Moved some claim finding logic to a helper method --- src/Api/Controllers/AccountsController.cs | 43 +++ src/Core/IdentityServer/ClientStore.cs | 59 +++- .../CustomTokenRequestValidator.cs | 8 +- src/Core/IdentityServer/ProfileService.cs | 56 +--- .../Api/Response/ApiKeyResponseModel.cs | 10 + src/Core/Models/Table/User.cs | 3 +- src/Core/Services/IUserService.cs | 1 + .../Services/Implementations/UserService.cs | 8 + src/Core/Utilities/CoreHelpers.cs | 56 ++++ src/Sql/dbo/Stored Procedures/User_Create.sql | 9 +- src/Sql/dbo/Stored Procedures/User_Update.sql | 6 +- src/Sql/dbo/Tables/User.sql | 1 + .../Controllers/AccountsControllerTests.cs | 60 ++++ .../DbScripts/2020-10-28_00_UserApiKey.sql | 279 ++++++++++++++++++ 14 files changed, 540 insertions(+), 59 deletions(-) create mode 100644 util/Migrator/DbScripts/2020-10-28_00_UserApiKey.sql diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index e14220f30..5d50881ca 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -734,5 +734,48 @@ namespace Bit.Api.Controllers var userIdentifier = $"{user.Id},{token}"; return userIdentifier; } + + [HttpPost("api-key")] + public async Task ApiKey([FromBody]ApiKeyRequestModel model) + { + var user = await _userService.GetUserByPrincipalAsync(User); + if (user == null) + { + throw new UnauthorizedAccessException(); + } + + if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash)) + { + await Task.Delay(2000); + throw new BadRequestException("MasterPasswordHash", "Invalid password."); + } + else + { + var response = new ApiKeyResponseModel(user); + return response; + } + } + + [HttpPost("rotate-api-key")] + public async Task RotateApiKey([FromBody]ApiKeyRequestModel model) + { + var user = await _userService.GetUserByPrincipalAsync(User); + if (user == null) + { + throw new UnauthorizedAccessException(); + } + + if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash)) + { + await Task.Delay(2000); + throw new BadRequestException("MasterPasswordHash", "Invalid password."); + } + else + { + await _userService.RotateApiKeyAsync(user); + var response = new ApiKeyResponseModel(user); + return response; + } + } } } diff --git a/src/Core/IdentityServer/ClientStore.cs b/src/Core/IdentityServer/ClientStore.cs index bc58d73b2..9e8e6292c 100644 --- a/src/Core/IdentityServer/ClientStore.cs +++ b/src/Core/IdentityServer/ClientStore.cs @@ -1,4 +1,5 @@ -using IdentityServer4.Stores; +using System.Linq; +using IdentityServer4.Stores; using System.Threading.Tasks; using IdentityServer4.Models; using System.Collections.Generic; @@ -6,6 +7,9 @@ using Bit.Core.Repositories; using System; using IdentityModel; using Bit.Core.Utilities; +using System.Security.Claims; +using Bit.Core.Services; +using System.Collections.ObjectModel; namespace Bit.Core.IdentityServer { @@ -13,19 +17,31 @@ namespace Bit.Core.IdentityServer { private readonly IInstallationRepository _installationRepository; private readonly IOrganizationRepository _organizationRepository; + private readonly IUserRepository _userRepository; private readonly GlobalSettings _globalSettings; private readonly StaticClientStore _staticClientStore; + private readonly ILicensingService _licensingService; + private readonly CurrentContext _currentContext; + private readonly IOrganizationUserRepository _organizationUserRepository; public ClientStore( IInstallationRepository installationRepository, IOrganizationRepository organizationRepository, + IUserRepository userRepository, GlobalSettings globalSettings, - StaticClientStore staticClientStore) + StaticClientStore staticClientStore, + ILicensingService licensingService, + CurrentContext currentContext, + IOrganizationUserRepository organizationUserRepository) { _installationRepository = installationRepository; _organizationRepository = organizationRepository; + _userRepository = userRepository; _globalSettings = globalSettings; _staticClientStore = staticClientStore; + _licensingService = licensingService; + _currentContext = currentContext; + _organizationUserRepository = organizationUserRepository; } public async Task FindClientByIdAsync(string clientId) @@ -106,6 +122,45 @@ namespace Bit.Core.IdentityServer } } } + else if (clientId.StartsWith("user.")) + { + var idParts = clientId.Split('.'); + if (idParts.Length > 1 && Guid.TryParse(idParts[1], out var id)) + { + var user = await _userRepository.GetByIdAsync(id); + if (user != null) + { + var claims = new Collection() + { + new ClientClaim(JwtClaimTypes.Subject, user.Id.ToString()), + new ClientClaim(JwtClaimTypes.AuthenticationMethod, "Application", "external") + }; + var orgs = await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id); + var isPremium = await _licensingService.ValidateUserPremiumAsync(user); + foreach (var claim in CoreHelpers.BuildIdentityClaims(user, orgs, isPremium)) + { + var upperValue = claim.Value.ToUpperInvariant(); + var isBool = upperValue == "TRUE" || upperValue == "FALSE"; + claims.Add(isBool ? + new ClientClaim(claim.Key, claim.Value, ClaimValueTypes.Boolean) : + new ClientClaim(claim.Key, claim.Value) + ); + } + + return new Client + { + ClientId = clientId, + RequireClientSecret = true, + ClientSecrets = { new Secret(user.ApiKey.Sha256()) }, + AllowedScopes = new string[] { "api" }, + AllowedGrantTypes = GrantTypes.ClientCredentials, + AccessTokenLifetime = 3600 * 1, + ClientClaimsPrefix = null, + Claims = claims + }; + } + } + } return _staticClientStore.ApiClients.ContainsKey(clientId) ? _staticClientStore.ApiClients[clientId] : null; diff --git a/src/Core/IdentityServer/CustomTokenRequestValidator.cs b/src/Core/IdentityServer/CustomTokenRequestValidator.cs index f5246d626..7032e6c88 100644 --- a/src/Core/IdentityServer/CustomTokenRequestValidator.cs +++ b/src/Core/IdentityServer/CustomTokenRequestValidator.cs @@ -10,6 +10,7 @@ using System.Linq; using Bit.Core.Identity; using Microsoft.Extensions.Logging; using IdentityServer4.Extensions; +using IdentityModel; namespace Bit.Core.IdentityServer { @@ -42,7 +43,8 @@ namespace Bit.Core.IdentityServer public async Task ValidateAsync(CustomTokenRequestValidationContext context) { - if (context.Result.ValidatedRequest.GrantType != "authorization_code") + string[] allowedGrantTypes = { "authorization_code", "client_credentials" }; + if (!allowedGrantTypes.Contains(context.Result.ValidatedRequest.GrantType)) { return; } @@ -51,7 +53,9 @@ namespace Bit.Core.IdentityServer protected async override Task<(User, bool)> ValidateContextAsync(CustomTokenRequestValidationContext context) { - var user = await _userManager.FindByEmailAsync(context.Result.ValidatedRequest.Subject.GetDisplayName()); + var email = context.Result.ValidatedRequest.Subject?.GetDisplayName() + ?? context.Result.ValidatedRequest.ClientClaims.FirstOrDefault(claim => claim.Type == JwtClaimTypes.Email).Value; + var user = await _userManager.FindByEmailAsync(email); return (user, user != null); } diff --git a/src/Core/IdentityServer/ProfileService.cs b/src/Core/IdentityServer/ProfileService.cs index 4d2affd36..1e0d3ce19 100644 --- a/src/Core/IdentityServer/ProfileService.cs +++ b/src/Core/IdentityServer/ProfileService.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Linq; using System; using IdentityModel; +using Bit.Core.Utilities; namespace Bit.Core.IdentityServer { @@ -39,56 +40,15 @@ namespace Bit.Core.IdentityServer if (user != null) { var isPremium = await _licensingService.ValidateUserPremiumAsync(user); - newClaims.AddRange(new List - { - new Claim("premium", isPremium ? "true" : "false", ClaimValueTypes.Boolean), - new Claim(JwtClaimTypes.Email, user.Email), - new Claim(JwtClaimTypes.EmailVerified, user.EmailVerified ? "true" : "false", - ClaimValueTypes.Boolean), - new Claim("sstamp", user.SecurityStamp) - }); - - if (!string.IsNullOrWhiteSpace(user.Name)) - { - newClaims.Add(new Claim(JwtClaimTypes.Name, user.Name)); - } - - // Orgs that this user belongs to var orgs = await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id); - if (orgs.Any()) + foreach (var claim in CoreHelpers.BuildIdentityClaims(user, orgs, isPremium)) { - foreach (var group in orgs.GroupBy(o => o.Type)) - { - switch (group.Key) - { - case Enums.OrganizationUserType.Owner: - foreach (var org in group) - { - newClaims.Add(new Claim("orgowner", org.Id.ToString())); - } - break; - case Enums.OrganizationUserType.Admin: - foreach (var org in group) - { - newClaims.Add(new Claim("orgadmin", org.Id.ToString())); - } - break; - case Enums.OrganizationUserType.Manager: - foreach (var org in group) - { - newClaims.Add(new Claim("orgmanager", org.Id.ToString())); - } - break; - case Enums.OrganizationUserType.User: - foreach (var org in group) - { - newClaims.Add(new Claim("orguser", org.Id.ToString())); - } - break; - default: - break; - } - } + var upperValue = claim.Value.ToUpperInvariant(); + var isBool = upperValue == "TRUE" || upperValue == "FALSE"; + newClaims.Add(isBool ? + new Claim(claim.Key, claim.Value, ClaimValueTypes.Boolean) : + new Claim(claim.Key, claim.Value) + ); } } diff --git a/src/Core/Models/Api/Response/ApiKeyResponseModel.cs b/src/Core/Models/Api/Response/ApiKeyResponseModel.cs index 0a69a7d34..c64db1c0e 100644 --- a/src/Core/Models/Api/Response/ApiKeyResponseModel.cs +++ b/src/Core/Models/Api/Response/ApiKeyResponseModel.cs @@ -15,6 +15,16 @@ namespace Bit.Core.Models.Api ApiKey = organization.ApiKey; } + public ApiKeyResponseModel(User user, string obj = "apiKey") + : base(obj) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + ApiKey = user.ApiKey; + } + public string ApiKey { get; set; } } } diff --git a/src/Core/Models/Table/User.cs b/src/Core/Models/Table/User.cs index 85417adab..20513e096 100644 --- a/src/Core/Models/Table/User.cs +++ b/src/Core/Models/Table/User.cs @@ -3,8 +3,6 @@ using Bit.Core.Enums; using Bit.Core.Utilities; using System.Collections.Generic; using Newtonsoft.Json; -using Bit.Core.Services; -using Bit.Core.Exceptions; using Microsoft.AspNetCore.Identity; namespace Bit.Core.Models.Table @@ -39,6 +37,7 @@ namespace Bit.Core.Models.Table public string GatewaySubscriptionId { get; set; } public string ReferenceData { get; set; } public string LicenseKey { get; set; } + public string ApiKey { get; set; } public KdfType Kdf { get; set; } = KdfType.PBKDF2_SHA256; public int KdfIterations { get; set; } = 5000; public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 78fafe688..c2830f6ef 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -69,5 +69,6 @@ namespace Bit.Core.Services Task TwoFactorProviderIsEnabledAsync(TwoFactorProviderType provider, ITwoFactorProvidersUser user); Task GenerateEnterprisePortalSignInTokenAsync(User user); Task GenerateSignInTokenAsync(User user, string purpose); + Task RotateApiKeyAsync(User user); } } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 66e803620..bbe602c16 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -292,6 +292,7 @@ namespace Bit.Core.Services } } + user.ApiKey = CoreHelpers.SecureRandomString(30); var result = await base.CreateAsync(user, masterPassword); if (result == IdentityResult.Success) { @@ -1204,5 +1205,12 @@ namespace Bit.Core.Services } return result; } + + public async Task RotateApiKeyAsync(User user) + { + user.ApiKey = CoreHelpers.SecureRandomString(30); + user.RevisionDate = DateTime.UtcNow; + await _userRepository.ReplaceAsync(user); + } } } diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index 684e49448..2689d78d4 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -18,6 +18,8 @@ using Bit.Core.Enums; using System.Threading.Tasks; using Microsoft.Azure.Storage; using Microsoft.Azure.Storage.Blob; +using Bit.Core.Models.Table; +using IdentityModel; namespace Bit.Core.Utilities { @@ -670,5 +672,59 @@ namespace Bit.Core.Utilities } return configDict; } + + public static Dictionary BuildIdentityClaims(User user, ICollection orgs, bool isPremium) + { + var claims = new Dictionary() + { + {"premium", isPremium ? "true" : "false"}, + {JwtClaimTypes.Email, user.Email}, + {JwtClaimTypes.EmailVerified, user.EmailVerified ? "true" : "false"}, + {"sstamp", user.SecurityStamp} + }; + + if (!string.IsNullOrWhiteSpace(user.Name)) + { + claims.Add(JwtClaimTypes.Name, user.Name); + } + + // Orgs that this user belongs to + if (orgs.Any()) + { + foreach (var group in orgs.GroupBy(o => o.Type)) + { + switch (group.Key) + { + case Enums.OrganizationUserType.Owner: + foreach (var org in group) + { + claims.Add("orgowner", org.Id.ToString()); + } + break; + case Enums.OrganizationUserType.Admin: + foreach (var org in group) + { + claims.Add("orgadmin", org.Id.ToString()); + } + break; + case Enums.OrganizationUserType.Manager: + foreach (var org in group) + { + claims.Add("orgmanager", org.Id.ToString()); + } + break; + case Enums.OrganizationUserType.User: + foreach (var org in group) + { + claims.Add("orguser", org.Id.ToString()); + } + break; + default: + break; + } + } + } + return claims; + } } } diff --git a/src/Sql/dbo/Stored Procedures/User_Create.sql b/src/Sql/dbo/Stored Procedures/User_Create.sql index 07f75516d..98a176c52 100644 --- a/src/Sql/dbo/Stored Procedures/User_Create.sql +++ b/src/Sql/dbo/Stored Procedures/User_Create.sql @@ -28,7 +28,8 @@ @Kdf TINYINT, @KdfIterations INT, @CreationDate DATETIME2(7), - @RevisionDate DATETIME2(7) + @RevisionDate DATETIME2(7), + @ApiKey VARCHAR(30) AS BEGIN SET NOCOUNT ON @@ -64,7 +65,8 @@ BEGIN [Kdf], [KdfIterations], [CreationDate], - [RevisionDate] + [RevisionDate], + [ApiKey] ) VALUES ( @@ -97,6 +99,7 @@ BEGIN @Kdf, @KdfIterations, @CreationDate, - @RevisionDate + @RevisionDate, + @ApiKey ) END diff --git a/src/Sql/dbo/Stored Procedures/User_Update.sql b/src/Sql/dbo/Stored Procedures/User_Update.sql index d484cf0ce..25e2eca38 100644 --- a/src/Sql/dbo/Stored Procedures/User_Update.sql +++ b/src/Sql/dbo/Stored Procedures/User_Update.sql @@ -28,7 +28,8 @@ @Kdf TINYINT, @KdfIterations INT, @CreationDate DATETIME2(7), - @RevisionDate DATETIME2(7) + @RevisionDate DATETIME2(7), + @ApiKey VARCHAR(30) AS BEGIN SET NOCOUNT ON @@ -64,7 +65,8 @@ BEGIN [Kdf] = @Kdf, [KdfIterations] = @KdfIterations, [CreationDate] = @CreationDate, - [RevisionDate] = @RevisionDate + [RevisionDate] = @RevisionDate, + [ApiKey] = @ApiKey WHERE [Id] = @Id END diff --git a/src/Sql/dbo/Tables/User.sql b/src/Sql/dbo/Tables/User.sql index 52b01ec84..b11657c6f 100644 --- a/src/Sql/dbo/Tables/User.sql +++ b/src/Sql/dbo/Tables/User.sql @@ -29,6 +29,7 @@ [KdfIterations] INT NOT NULL, [CreationDate] DATETIME2 (7) NOT NULL, [RevisionDate] DATETIME2 (7) NOT NULL, + [ApiKey] VARCHAR (30) NOT NULL, CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC) ); diff --git a/test/Api.Test/Controllers/AccountsControllerTests.cs b/test/Api.Test/Controllers/AccountsControllerTests.cs index 8d083d6c1..cabad8f4b 100644 --- a/test/Api.Test/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Controllers/AccountsControllerTests.cs @@ -304,6 +304,66 @@ namespace Bit.Api.Test.Controllers ); } + [Fact] + public async Task GetApiKey_ShouldReturnApiKeyResponse() + { + var user = GenerateExampleUser(); + ConfigureUserServiceToReturnValidPrincipalFor(user); + ConfigureUserServiceToAcceptPasswordFor(user); + await _sut.ApiKey(new ApiKeyRequestModel()); + } + + [Fact] + public async Task GetApiKey_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException() + { + ConfigureUserServiceToReturnNullPrincipal(); + + await Assert.ThrowsAsync( + () => _sut.ApiKey(new ApiKeyRequestModel()) + ); + } + + [Fact] + public async Task GetApiKey_WhenPasswordCheckFails_ShouldThrowBadRequestException() + { + var user = GenerateExampleUser(); + ConfigureUserServiceToReturnValidPrincipalFor(user); + ConfigureUserServiceToRejectPasswordFor(user); + await Assert.ThrowsAsync( + () => _sut.ApiKey(new ApiKeyRequestModel()) + ); + } + + [Fact] + public async Task PostRotateApiKey_ShouldRotateApiKey() + { + var user = GenerateExampleUser(); + ConfigureUserServiceToReturnValidPrincipalFor(user); + ConfigureUserServiceToAcceptPasswordFor(user); + await _sut.RotateApiKey(new ApiKeyRequestModel()); + } + + [Fact] + public async Task PostRotateApiKey_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException() + { + ConfigureUserServiceToReturnNullPrincipal(); + + await Assert.ThrowsAsync( + () => _sut.ApiKey(new ApiKeyRequestModel()) + ); + } + + [Fact] + public async Task PostRotateApiKey_WhenPasswordCheckFails_ShouldThrowBadRequestException() + { + var user = GenerateExampleUser(); + ConfigureUserServiceToReturnValidPrincipalFor(user); + ConfigureUserServiceToRejectPasswordFor(user); + await Assert.ThrowsAsync( + () => _sut.ApiKey(new ApiKeyRequestModel()) + ); + } + // Below are helper functions that currently belong to this // test class, but ultimately may need to be split out into // something greater in order to share common test steps with diff --git a/util/Migrator/DbScripts/2020-10-28_00_UserApiKey.sql b/util/Migrator/DbScripts/2020-10-28_00_UserApiKey.sql new file mode 100644 index 000000000..c712e7cac --- /dev/null +++ b/util/Migrator/DbScripts/2020-10-28_00_UserApiKey.sql @@ -0,0 +1,279 @@ +-- Add ApiKey column to dbo.User, nullable for now but will be not null after backfilling +IF COL_LENGTH('[dbo].[User]', 'ApiKey') IS NULL +BEGIN + ALTER TABLE + [dbo].[User] + ADD + [ApiKey] VARCHAR (30) NULL +END +GO + +-- Setup for random string generation to backfill dbo.User.ApiKey +CREATE VIEW [dbo].[SecureRandomBytes] +AS +SELECT [RandBytes] = CRYPT_GEN_RANDOM(2) +GO + +CREATE FUNCTION [dbo].[SecureRandomString]() +RETURNS varchar(30) +AS +BEGIN + declare @sLength tinyint + declare @randomString varchar(30) + declare @counter tinyint + declare @nextChar char(1) + declare @rnd as float + declare @bytes binary(2) + + + set @sLength = 30 + set @counter = 1 + set @randomString = '' + + + while @counter <= @sLength + begin + select @bytes = [RandBytes] from [dbo].[SecureRandomBytes] + select @rnd = cast(cast(cast(@bytes as int) as float) / 65535 as float) + select @nextChar = char(48 + convert(int, (122-48+1) * @rnd)) + if ascii(@nextChar) not in (58,59,60,61,62,63,64,91,92,93,94,95,96) + begin + select @randomString = @randomString + @nextChar + set @counter = @counter + 1 + end + end + return @randomString +END +GO + +-- Backfill dbo.User.ApiKey +UPDATE + [dbo].[User] +SET + [ApiKey] = (SELECT [dbo].[SecureRandomString]()) +GO + +-- Change dbo.User.ApiKey to not null to enforece all future users to have one on create +ALTER TABLE + [dbo].[User] +ALTER COLUMN + [ApiKey] VARCHAR(30) NOT NULL +GO + + +-- Cleanup random string generation +DROP VIEW [dbo].[SecureRandomBytes] +GO +DROP FUNCTION [dbo].[SecureRandomString] +GO + +-- Update dbo.User_Create to account for ApiKey +IF OBJECT_ID('[dbo].[User_Create]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[User_Create] +END +GO + +CREATE PROCEDURE [dbo].[User_Create] + @Id UNIQUEIDENTIFIER, + @Name NVARCHAR(50), + @Email NVARCHAR(50), + @EmailVerified BIT, + @MasterPassword NVARCHAR(300), + @MasterPasswordHint NVARCHAR(50), + @Culture NVARCHAR(10), + @SecurityStamp NVARCHAR(50), + @TwoFactorProviders NVARCHAR(MAX), + @TwoFactorRecoveryCode NVARCHAR(32), + @EquivalentDomains NVARCHAR(MAX), + @ExcludedGlobalEquivalentDomains NVARCHAR(MAX), + @AccountRevisionDate DATETIME2(7), + @Key NVARCHAR(MAX), + @PublicKey NVARCHAR(MAX), + @PrivateKey NVARCHAR(MAX), + @Premium BIT, + @PremiumExpirationDate DATETIME2(7), + @RenewalReminderDate DATETIME2(7), + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @LicenseKey VARCHAR(100), + @Kdf TINYINT, + @KdfIterations INT, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @ApiKey VARCHAR(30) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[User] + ( + [Id], + [Name], + [Email], + [EmailVerified], + [MasterPassword], + [MasterPasswordHint], + [Culture], + [SecurityStamp], + [TwoFactorProviders], + [TwoFactorRecoveryCode], + [EquivalentDomains], + [ExcludedGlobalEquivalentDomains], + [AccountRevisionDate], + [Key], + [PublicKey], + [PrivateKey], + [Premium], + [PremiumExpirationDate], + [RenewalReminderDate], + [Storage], + [MaxStorageGb], + [Gateway], + [GatewayCustomerId], + [GatewaySubscriptionId], + [ReferenceData], + [LicenseKey], + [Kdf], + [KdfIterations], + [CreationDate], + [RevisionDate], + [ApiKey] + ) + VALUES + ( + @Id, + @Name, + @Email, + @EmailVerified, + @MasterPassword, + @MasterPasswordHint, + @Culture, + @SecurityStamp, + @TwoFactorProviders, + @TwoFactorRecoveryCode, + @EquivalentDomains, + @ExcludedGlobalEquivalentDomains, + @AccountRevisionDate, + @Key, + @PublicKey, + @PrivateKey, + @Premium, + @PremiumExpirationDate, + @RenewalReminderDate, + @Storage, + @MaxStorageGb, + @Gateway, + @GatewayCustomerId, + @GatewaySubscriptionId, + @ReferenceData, + @LicenseKey, + @Kdf, + @KdfIterations, + @CreationDate, + @RevisionDate, + @ApiKey + ) +END +GO + +-- Update dbo.User_Update to account for ApiKey +IF OBJECT_ID('[dbo].[User_Update]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[User_Update] +END +GO + +CREATE PROCEDURE [dbo].[User_Update] + @Id UNIQUEIDENTIFIER, + @Name NVARCHAR(50), + @Email NVARCHAR(50), + @EmailVerified BIT, + @MasterPassword NVARCHAR(300), + @MasterPasswordHint NVARCHAR(50), + @Culture NVARCHAR(10), + @SecurityStamp NVARCHAR(50), + @TwoFactorProviders NVARCHAR(MAX), + @TwoFactorRecoveryCode NVARCHAR(32), + @EquivalentDomains NVARCHAR(MAX), + @ExcludedGlobalEquivalentDomains NVARCHAR(MAX), + @AccountRevisionDate DATETIME2(7), + @Key NVARCHAR(MAX), + @PublicKey NVARCHAR(MAX), + @PrivateKey NVARCHAR(MAX), + @Premium BIT, + @PremiumExpirationDate DATETIME2(7), + @RenewalReminderDate DATETIME2(7), + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @LicenseKey VARCHAR(100), + @Kdf TINYINT, + @KdfIterations INT, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @ApiKey VARCHAR(30) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[User] + SET + [Name] = @Name, + [Email] = @Email, + [EmailVerified] = @EmailVerified, + [MasterPassword] = @MasterPassword, + [MasterPasswordHint] = @MasterPasswordHint, + [Culture] = @Culture, + [SecurityStamp] = @SecurityStamp, + [TwoFactorProviders] = @TwoFactorProviders, + [TwoFactorRecoveryCode] = @TwoFactorRecoveryCode, + [EquivalentDomains] = @EquivalentDomains, + [ExcludedGlobalEquivalentDomains] = @ExcludedGlobalEquivalentDomains, + [AccountRevisionDate] = @AccountRevisionDate, + [Key] = @Key, + [PublicKey] = @PublicKey, + [PrivateKey] = @PrivateKey, + [Premium] = @Premium, + [PremiumExpirationDate] = @PremiumExpirationDate, + [RenewalReminderDate] = @RenewalReminderDate, + [Storage] = @Storage, + [MaxStorageGb] = @MaxStorageGb, + [Gateway] = @Gateway, + [GatewayCustomerId] = @GatewayCustomerId, + [GatewaySubscriptionId] = @GatewaySubscriptionId, + [ReferenceData] = @ReferenceData, + [LicenseKey] = @LicenseKey, + [Kdf] = @Kdf, + [KdfIterations] = @KdfIterations, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [ApiKey] = @ApiKey + WHERE + [Id] = @Id +END +GO + +-- Refresh dbo.UserView so it has access to ApiKey +IF OBJECT_ID('[dbo].[UserView]') IS NOT NULL +BEGIN + DROP VIEW [dbo].[UserView] +END +GO + +CREATE VIEW [dbo].[UserView] +AS +SELECT + * +FROM + [dbo].[User] + +GO