1
0
mirror of https://github.com/bitwarden/server synced 2026-01-08 19:43:34 +00:00

Merge branch 'master' into feature/org-admin-refresh

This commit is contained in:
Shane Melton
2022-10-27 16:44:58 -07:00
26 changed files with 3724 additions and 33 deletions

View File

@@ -131,15 +131,13 @@ public class AuthRequestsController : Controller
throw new BadRequestException("Invalid device.");
}
if (model.RequestApproved)
{
authRequest.Key = model.Key;
authRequest.MasterPasswordHash = model.MasterPasswordHash;
authRequest.ResponseDeviceId = device.Id;
authRequest.ResponseDate = DateTime.UtcNow;
await _authRequestRepository.ReplaceAsync(authRequest);
await _pushNotificationService.PushAuthRequestResponseAsync(authRequest);
}
authRequest.Key = model.Key;
authRequest.MasterPasswordHash = model.MasterPasswordHash;
authRequest.ResponseDeviceId = device.Id;
authRequest.ResponseDate = DateTime.UtcNow;
authRequest.Approved = model.RequestApproved;
await _authRequestRepository.ReplaceAsync(authRequest);
await _pushNotificationService.PushAuthRequestResponseAsync(authRequest);
return new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);
}

View File

@@ -272,11 +272,16 @@ public class CiphersController : Controller
[HttpPut("{id}/partial")]
[HttpPost("{id}/partial")]
public async Task PutPartial(string id, [FromBody] CipherPartialRequestModel model)
public async Task<CipherResponseModel> PutPartial(string id, [FromBody] CipherPartialRequestModel model)
{
var userId = _userService.GetProperUserId(User).Value;
var folderId = string.IsNullOrWhiteSpace(model.FolderId) ? null : (Guid?)new Guid(model.FolderId);
await _cipherRepository.UpdatePartialAsync(new Guid(id), userId, folderId, model.Favorite);
var cipherId = new Guid(id);
await _cipherRepository.UpdatePartialAsync(cipherId, userId, folderId, model.Favorite);
var cipher = await _cipherRepository.GetByIdAsync(cipherId, userId);
var response = new CipherResponseModel(cipher, _globalSettings);
return response;
}
[HttpPut("{id}/share")]

View File

@@ -1,7 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Api;
namespace Bit.Api.Models.Response;
@@ -25,8 +24,7 @@ public class AuthRequestResponseModel : ResponseModel
Key = authRequest.Key;
MasterPasswordHash = authRequest.MasterPasswordHash;
CreationDate = authRequest.CreationDate;
RequestApproved = !string.IsNullOrWhiteSpace(Key) &&
(authRequest.Type == AuthRequestType.Unlock || !string.IsNullOrWhiteSpace(MasterPasswordHash));
RequestApproved = authRequest.Approved ?? false;
Origin = new Uri(vaultUri).Host;
}

View File

@@ -20,6 +20,7 @@ public class AuthRequest : ITableObject<Guid>
public string PublicKey { get; set; }
public string Key { get; set; }
public string MasterPasswordHash { get; set; }
public bool? Approved { get; set; }
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
public DateTime? ResponseDate { get; set; }
public DateTime? AuthenticationDate { get; set; }

View File

@@ -91,4 +91,5 @@ public enum GlobalEquivalentDomainsType : byte
Ubisoft = 86,
TransferWise = 87,
TakeawayEU = 88,
Atlassian = 89,
}

View File

@@ -100,6 +100,7 @@ public class StaticStore
GlobalDomains.Add(GlobalEquivalentDomainsType.Ubisoft, new List<string> { "ubisoft.com", "ubi.com" });
GlobalDomains.Add(GlobalEquivalentDomainsType.TransferWise, new List<string> { "transferwise.com", "wise.com" });
GlobalDomains.Add(GlobalEquivalentDomainsType.TakeawayEU, new List<string> { "takeaway.com", "just-eat.dk", "just-eat.no", "just-eat.fr", "just-eat.ch", "lieferando.de", "lieferando.at", "thuisbezorgd.nl", "pyszne.pl" });
GlobalDomains.Add(GlobalEquivalentDomainsType.Atlassian, new List<string> { "atlassian.com", "bitbucket.org", "trello.com", "statuspage.io", "atlassian.net", "jira.com" });
#endregion
#region Plans

View File

@@ -124,8 +124,8 @@ public abstract class BaseEntityFrameworkRepository
!string.IsNullOrWhiteSpace(e.Attachments))
.Select(e => e.Attachments)
.ToListAsync();
var storage = attachments.Sum(e => JsonDocument.Parse(e)?.RootElement.EnumerateArray()
.Sum(p => p.GetProperty("Size").GetInt64()) ?? 0);
var storage = attachments.Sum(e => JsonDocument.Parse(e)?.RootElement.EnumerateObject()
.Sum(p => p.Value.GetProperty("Size").GetInt64()) ?? 0);
var organization = new Organization
{
Id = organizationId,
@@ -152,8 +152,8 @@ public abstract class BaseEntityFrameworkRepository
!string.IsNullOrWhiteSpace(e.Attachments))
.Select(e => e.Attachments)
.ToListAsync();
var storage = attachments.Sum(e => JsonDocument.Parse(e)?.RootElement.EnumerateArray()
.Sum(p => p.GetProperty("Size").GetInt64()) ?? 0);
var storage = attachments.Sum(e => JsonDocument.Parse(e)?.RootElement.EnumerateObject()
.Sum(p => p.Value.GetProperty("Size").GetInt64()) ?? 0);
var user = new Models.User
{
Id = userId,

View File

@@ -261,7 +261,7 @@ public class CipherRepository : Repository<Core.Entities.Cipher, Cipher, Guid>,
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var query = new CipherOrganizationDetailsReadByIdQuery(organizationId);
var query = new CipherOrganizationDetailsReadByOrganizationIdQuery(organizationId);
var data = await query.Run(dbContext).ToListAsync();
return data;
}
@@ -345,7 +345,6 @@ public class CipherRepository : Repository<Core.Entities.Cipher, Cipher, Guid>,
var idsToMove = from ucd in userCipherDetails
join c in cipherEntities
on ucd.Id equals c.Id
where ucd.Edit
select c;
await idsToMove.ForEachAsync(cipher =>
{
@@ -478,6 +477,16 @@ public class CipherRepository : Repository<Core.Entities.Cipher, Cipher, Guid>,
private async Task<DateTime> ToggleCipherStates(IEnumerable<Guid> ids, Guid userId, CipherStateAction action)
{
static bool FilterDeletedDate(CipherStateAction action, CipherDetails ucd)
{
return action switch
{
CipherStateAction.Restore => ucd.DeletedDate != null,
CipherStateAction.SoftDelete => ucd.DeletedDate == null,
_ => true,
};
}
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
@@ -486,7 +495,7 @@ public class CipherRepository : Repository<Core.Entities.Cipher, Cipher, Guid>,
var query = from ucd in await (userCipherDetailsQuery.Run(dbContext)).ToListAsync()
join c in cipherEntitiesToCheck
on ucd.Id equals c.Id
where ucd.Edit && ucd.DeletedDate == null
where ucd.Edit && FilterDeletedDate(action, ucd)
select c;
var utcNow = DateTime.UtcNow;
@@ -550,9 +559,11 @@ public class CipherRepository : Repository<Core.Entities.Cipher, Cipher, Guid>,
{
var dbContext = GetDatabaseContext(scope);
var cipher = await dbContext.Ciphers.FindAsync(attachment.Id);
var attachmentsJson = string.IsNullOrWhiteSpace(cipher.Attachments) ? new JObject() : JObject.Parse(cipher.Attachments);
attachmentsJson.Add(attachment.AttachmentId, attachment.AttachmentData);
cipher.Attachments = JsonConvert.SerializeObject(attachmentsJson);
var attachments = string.IsNullOrWhiteSpace(cipher.Attachments) ?
new Dictionary<string, CipherAttachment.MetaData>() :
JsonConvert.DeserializeObject<Dictionary<string, CipherAttachment.MetaData>>(cipher.Attachments);
attachments.Add(attachment.AttachmentId, JsonConvert.DeserializeObject<CipherAttachment.MetaData>(attachment.AttachmentData));
cipher.Attachments = JsonConvert.SerializeObject(attachments);
await dbContext.SaveChangesAsync();
if (attachment.OrganizationId.HasValue)

View File

@@ -2,11 +2,11 @@
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
public class CipherOrganizationDetailsReadByOrgizationIdQuery : IQuery<CipherOrganizationDetails>
public class CipherOrganizationDetailsReadByOrganizationIdQuery : IQuery<CipherOrganizationDetails>
{
private readonly Guid _organizationId;
public CipherOrganizationDetailsReadByOrgizationIdQuery(Guid organizationId)
public CipherOrganizationDetailsReadByOrganizationIdQuery(Guid organizationId)
{
_organizationId = organizationId;
}

View File

@@ -1,6 +1,6 @@
using Bit.Core.Enums;
using System.Text.Json;
using Bit.Core.Enums;
using Core.Models.Data;
using Newtonsoft.Json.Linq;
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
@@ -59,13 +59,24 @@ public class UserCipherDetailsQuery : IQuery<CipherDetails>
RevisionDate = c.RevisionDate,
DeletedDate = c.DeletedDate,
Favorite = _userId.HasValue && c.Favorites != null && c.Favorites.Contains($"\"{_userId}\":true"),
FolderId = _userId.HasValue && !string.IsNullOrWhiteSpace(c.Folders) ?
Guid.Parse(JObject.Parse(c.Folders)[_userId.Value.ToString()].Value<string>()) :
null,
FolderId = GetFolderId(_userId, c),
Edit = true,
ViewPassword = true,
OrganizationUseTotp = false,
});
return union;
}
private static Guid? GetFolderId(Guid? userId, Models.Cipher cipher)
{
if (userId.HasValue && !string.IsNullOrWhiteSpace(cipher.Folders))
{
var folders = JsonSerializer.Deserialize<Dictionary<Guid, Guid>>(cipher.Folders);
if (folders.TryGetValue(userId.Value, out var folder))
{
return folder;
}
}
return null;
}
}

View File

@@ -11,6 +11,7 @@
@PublicKey VARCHAR(MAX),
@Key VARCHAR(MAX),
@MasterPasswordHash VARCHAR(MAX),
@Approved BIT,
@CreationDate DATETIME2(7),
@ResponseDate DATETIME2(7),
@AuthenticationDate DATETIME2(7)
@@ -32,6 +33,7 @@ BEGIN
[PublicKey],
[Key],
[MasterPasswordHash],
[Approved],
[CreationDate],
[ResponseDate],
[AuthenticationDate]
@@ -50,6 +52,7 @@ BEGIN
@PublicKey,
@Key,
@MasterPasswordHash,
@Approved,
@CreationDate,
@ResponseDate,
@AuthenticationDate

View File

@@ -11,6 +11,7 @@
@PublicKey VARCHAR(MAX),
@Key VARCHAR(MAX),
@MasterPasswordHash VARCHAR(MAX),
@Approved BIT,
@CreationDate DATETIME2 (7),
@ResponseDate DATETIME2 (7),
@AuthenticationDate DATETIME2 (7)
@@ -32,6 +33,7 @@ BEGIN
[PublicKey] = @PublicKey,
[Key] = @Key,
[MasterPasswordHash] = @MasterPasswordHash,
[Approved] = @Approved,
[CreationDate] = @CreationDate,
[ResponseDate] = @ResponseDate,
[AuthenticationDate] = @AuthenticationDate

View File

@@ -15,8 +15,7 @@ BEGIN
FROM
[dbo].[UserCipherDetails](@UserId)
WHERE
[Edit] = 1
AND [Id] IN (SELECT * FROM @Ids)
[Id] IN (SELECT * FROM @Ids)
)
UPDATE
[dbo].[Cipher]

View File

@@ -11,6 +11,7 @@
[PublicKey] VARCHAR(MAX) NOT NULL,
[Key] VARCHAR(MAX) NULL,
[MasterPasswordHash] VARCHAR(MAX) NULL,
[Approved] BIT NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
[ResponseDate] DATETIME2 (7) NULL,
[AuthenticationDate] DATETIME2 (7) NULL,

View File

@@ -0,0 +1,45 @@
using System.Security.Claims;
using Bit.Api.Controllers;
using Bit.Api.Models.Request;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Core.Models.Data;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.Controllers;
[ControllerCustomize(typeof(CiphersController))]
[SutProviderCustomize]
public class CiphersControllerTests
{
[Theory, BitAutoData]
public async Task PutPartialShouldReturnCipherWithGivenFolderAndFavoriteValues(Guid userId, Guid folderId, SutProvider<CiphersController> sutProvider)
{
var isFavorite = true;
var cipherId = Guid.NewGuid();
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(userId);
var cipherDetails = new CipherDetails
{
Favorite = isFavorite,
FolderId = folderId,
Type = Core.Enums.CipherType.SecureNote,
Data = "{}"
};
sutProvider.GetDependency<ICipherRepository>()
.GetByIdAsync(cipherId, userId)
.Returns(Task.FromResult(cipherDetails));
var result = await sutProvider.Sut.PutPartial(cipherId.ToString(), new CipherPartialRequestModel { Favorite = isFavorite, FolderId = folderId.ToString() });
Assert.Equal(folderId.ToString(), result.FolderId);
Assert.Equal(isFavorite, result.Favorite);
}
}

View File

@@ -0,0 +1,39 @@
-- Remove check for Edit permission. User should be able to move the cipher to a different folder even if they don't have Edit permissions
ALTER PROCEDURE [dbo].[Cipher_Move]
@Ids AS [dbo].[GuidIdArray] READONLY,
@FolderId AS UNIQUEIDENTIFIER,
@UserId AS UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
DECLARE @UserIdKey VARCHAR(50) = CONCAT('"', @UserId, '"')
DECLARE @UserIdPath VARCHAR(50) = CONCAT('$.', @UserIdKey)
;WITH [IdsToMoveCTE] AS (
SELECT
[Id]
FROM
[dbo].[UserCipherDetails](@UserId)
WHERE
[Id] IN (SELECT * FROM @Ids)
)
UPDATE
[dbo].[Cipher]
SET
[Folders] =
CASE
WHEN @FolderId IS NOT NULL AND [Folders] IS NULL THEN
CONCAT('{', @UserIdKey, ':"', @FolderId, '"', '}')
WHEN @FolderId IS NOT NULL THEN
JSON_MODIFY([Folders], @UserIdPath, CAST(@FolderId AS VARCHAR(50)))
ELSE
JSON_MODIFY([Folders], @UserIdPath, NULL)
END
WHERE
[Id] IN (SELECT * FROM [IdsToMoveCTE])
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
END
GO

View File

@@ -0,0 +1,145 @@
--Add Column
IF COL_LENGTH('[dbo].[AuthRequest]', 'Approved') IS NULL
BEGIN
ALTER TABLE
[dbo].[AuthRequest]
ADD
[Approved] BIT NULL
END
GO
-- Drop and recreate view
IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'AuthRequestView')
BEGIN
DROP VIEW [dbo].[AuthRequestView]
END
GO
CREATE VIEW [dbo].[AuthRequestView]
AS
SELECT
*
FROM
[dbo].[AuthRequest]
GO
--Drop existing SPROC
IF OBJECT_ID('[dbo].[AuthRequest_Update]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[AuthRequest_Update]
END
GO
--Create SPROC with new column
CREATE PROCEDURE [dbo].[AuthRequest_Update]
@Id UNIQUEIDENTIFIER OUTPUT,
@UserId UNIQUEIDENTIFIER,
@Type SMALLINT,
@RequestDeviceIdentifier NVARCHAR(50),
@RequestDeviceType SMALLINT,
@RequestIpAddress VARCHAR(50),
@RequestFingerprint VARCHAR(MAX),
@ResponseDeviceId UNIQUEIDENTIFIER,
@AccessCode VARCHAR(25),
@PublicKey VARCHAR(MAX),
@Key VARCHAR(MAX),
@MasterPasswordHash VARCHAR(MAX),
@Approved BIT,
@CreationDate DATETIME2 (7),
@ResponseDate DATETIME2 (7),
@AuthenticationDate DATETIME2 (7)
AS
BEGIN
SET NOCOUNT ON
UPDATE
[dbo].[AuthRequest]
SET
[UserId] = @UserId,
[Type] = @Type,
[RequestDeviceIdentifier] = @RequestDeviceIdentifier,
[RequestDeviceType] = @RequestDeviceType,
[RequestIpAddress] = @RequestIpAddress,
[RequestFingerprint] = @RequestFingerprint,
[ResponseDeviceId] = @ResponseDeviceId,
[AccessCode] = @AccessCode,
[PublicKey] = @PublicKey,
[Key] = @Key,
[MasterPasswordHash] = @MasterPasswordHash,
[Approved] = @Approved,
[CreationDate] = @CreationDate,
[ResponseDate] = @ResponseDate,
[AuthenticationDate] = @AuthenticationDate
WHERE
[Id] = @Id
END
GO
--Drop existing SPROC
IF OBJECT_ID('[dbo].[AuthRequest_Create]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[AuthRequest_Create]
END
GO
--Create SPROC with new column
CREATE PROCEDURE [dbo].[AuthRequest_Create]
@Id UNIQUEIDENTIFIER OUTPUT,
@UserId UNIQUEIDENTIFIER,
@Type TINYINT,
@RequestDeviceIdentifier NVARCHAR(50),
@RequestDeviceType TINYINT,
@RequestIpAddress VARCHAR(50),
@RequestFingerprint VARCHAR(MAX),
@ResponseDeviceId UNIQUEIDENTIFIER,
@AccessCode VARCHAR(25),
@PublicKey VARCHAR(MAX),
@Key VARCHAR(MAX),
@MasterPasswordHash VARCHAR(MAX),
@Approved BIT,
@CreationDate DATETIME2(7),
@ResponseDate DATETIME2(7),
@AuthenticationDate DATETIME2(7)
AS
BEGIN
SET NOCOUNT ON
INSERT INTO [dbo].[AuthRequest]
(
[Id],
[UserId],
[Type],
[RequestDeviceIdentifier],
[RequestDeviceType],
[RequestIpAddress],
[RequestFingerprint],
[ResponseDeviceId],
[AccessCode],
[PublicKey],
[Key],
[MasterPasswordHash],
[Approved],
[CreationDate],
[ResponseDate],
[AuthenticationDate]
)
VALUES
(
@Id,
@UserId,
@Type,
@RequestDeviceIdentifier,
@RequestDeviceType,
@RequestIpAddress,
@RequestFingerprint,
@ResponseDeviceId,
@AccessCode,
@PublicKey,
@Key,
@MasterPasswordHash,
@Approved,
@CreationDate,
@ResponseDate,
@AuthenticationDate
)
END

View File

@@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.MySqlMigrations.Migrations;
public partial class PasswordlessAuthRequestAddApprovedColumn : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "Approved",
table: "AuthRequest",
type: "tinyint(1)",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Approved",
table: "AuthRequest");
}
}

View File

@@ -28,6 +28,9 @@ namespace Bit.MySqlMigrations.Migrations
.HasMaxLength(25)
.HasColumnType("varchar(25)");
b.Property<bool?>("Approved")
.HasColumnType("tinyint(1)");
b.Property<DateTime?>("AuthenticationDate")
.HasColumnType("datetime(6)");

View File

@@ -0,0 +1,8 @@
START TRANSACTION;
ALTER TABLE `AuthRequest` ADD `Approved` tinyint(1) NULL;
INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`)
VALUES ('20221024210500_PasswordlessAuthRequestAddApprovedColumn', '6.0.4');
COMMIT;

View File

@@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.PostgresMigrations.Migrations;
public partial class PasswordlessAuthRequestAddApprovedColumn : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "Approved",
table: "AuthRequest",
type: "boolean",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Approved",
table: "AuthRequest");
}
}

View File

@@ -30,6 +30,9 @@ namespace Bit.PostgresMigrations.Migrations
b.Property<string>("AccessCode")
.HasColumnType("text");
b.Property<bool?>("Approved")
.HasColumnType("boolean");
b.Property<DateTime?>("AuthenticationDate")
.HasColumnType("timestamp with time zone");

View File

@@ -0,0 +1,8 @@
START TRANSACTION;
ALTER TABLE "AuthRequest" ADD "Approved" boolean NULL;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20221025033204_PasswordlessAuthRequestAddApprovedColumn', '6.0.4');
COMMIT;