diff --git a/src/apps/Odin.Hosting/Controllers/OwnerToken/Drive/OwnerDriveManagementController.cs b/src/apps/Odin.Hosting/Controllers/OwnerToken/Drive/OwnerDriveManagementController.cs index f4297dda0c..5f7e1bf334 100644 --- a/src/apps/Odin.Hosting/Controllers/OwnerToken/Drive/OwnerDriveManagementController.cs +++ b/src/apps/Odin.Hosting/Controllers/OwnerToken/Drive/OwnerDriveManagementController.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; using System.Linq; +using System.Security.AccessControl; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Odin.Core; using Odin.Hosting.Controllers.Base; using Odin.Services.Authentication.Owner; +using Odin.Services.Authorization.Acl; using Odin.Services.Base; using Odin.Services.Base.SharedTypes; using Odin.Services.Drives; @@ -70,6 +72,14 @@ public async Task UpdateDriveMetadata([FromBody] UpdateDriveDefinitionRequ return true; } + [HttpPost("update-default-read-acl")] + public async Task UpdateDefaultReadAclAsync([FromBody] UpdateDriveDefinitionRequest request) + { + var driveId = await _driveManager.GetDriveIdByAliasAsync(request.TargetDrive, true); + await _driveManager.UpdateDefaultReadAclAsync(driveId.GetValueOrDefault(), request.DefaultReadAcl, WebOdinContext); + return true; + } + [HttpPost("UpdateAttributes")] public async Task UpdateDriveAttributes([FromBody] UpdateDriveDefinitionRequest request) { @@ -119,6 +129,8 @@ public class UpdateDriveDefinitionRequest public string Metadata { get; set; } public Dictionary Attributes { get; set; } + + public AccessControlList DefaultReadAcl { get; set; } } public class UpdateDriveReadModeRequest diff --git a/src/services/Odin.Services/Authorization/Acl/DriveAclAuthorizationService.cs b/src/services/Odin.Services/Authorization/Acl/DriveAclAuthorizationService.cs index e294ef5a5c..4b839f4d01 100644 --- a/src/services/Odin.Services/Authorization/Acl/DriveAclAuthorizationService.cs +++ b/src/services/Odin.Services/Authorization/Acl/DriveAclAuthorizationService.cs @@ -5,30 +5,39 @@ using Odin.Core.Exceptions; using Odin.Core.Identity; using Odin.Services.Base; +using Odin.Services.Drives.Management; using Odin.Services.Membership.Connections; namespace Odin.Services.Authorization.Acl { public class DriveAclAuthorizationService( CircleNetworkService circleNetwork, + DriveManager driveManager, ILogger logger) : IDriveAclAuthorizationService { - public async Task AssertCallerHasPermission(AccessControlList acl, IOdinContext odinContext) + public async Task AssertCallerMatchesAclAsync(Guid driveId, AccessControlList acl, IOdinContext odinContext) { - ThrowWhenFalse(await CallerHasPermission(acl, odinContext)); + ThrowWhenFalse(await CallerMatchesAclAsync(driveId, acl, odinContext)); } - public async Task IdentityHasPermissionAsync(OdinId odinId, AccessControlList acl, IOdinContext odinContext) + public async Task IdentityMatchesAclAsync(Guid driveId, OdinId odinId, AccessControlList acl, IOdinContext odinContext) { - //there must be an acl - if (acl == null) + var appliedAcl = acl; + + if (appliedAcl == null) { - return false; + appliedAcl = (await driveManager.GetDriveAsync(driveId)).DefaultReadAcl; + + //there must be an acl + if (appliedAcl == null) + { + return false; + } } //if file has required circles, see if caller has at least one - var requiredCircles = acl.GetRequiredCircles().ToList(); + var requiredCircles = appliedAcl.GetRequiredCircles().ToList(); if (requiredCircles.Any()) { var icr = await circleNetwork.GetIcrAsync(odinId, odinContext, true); @@ -41,17 +50,18 @@ public async Task IdentityHasPermissionAsync(OdinId odinId, AccessControlL //let it continue on } - var hasAtLeastOneCircle = requiredCircles.Intersect(icr.AccessGrant.CircleGrants?.Select(cg => cg.Value.CircleId.Value) ?? Array.Empty()) + var hasAtLeastOneCircle = requiredCircles + .Intersect(icr.AccessGrant.CircleGrants?.Select(cg => cg.Value.CircleId.Value) ?? Array.Empty()) .Any(); return hasAtLeastOneCircle; } - if (acl.GetRequiredIdentities().Any()) + if (appliedAcl.GetRequiredIdentities().Any()) { return false; } - switch (acl.RequiredSecurityGroup) + switch (appliedAcl.RequiredSecurityGroup) { case SecurityGroupType.Anonymous: return true; @@ -63,51 +73,57 @@ public async Task IdentityHasPermissionAsync(OdinId odinId, AccessControlL return false; } - public Task CallerHasPermission(AccessControlList acl, IOdinContext odinContext) + public async Task CallerMatchesAclAsync(Guid driveId, AccessControlList acl, IOdinContext odinContext) { var caller = odinContext.Caller; if (caller?.IsOwner ?? false) { - return Task.FromResult(true); + return true; } if (caller?.SecurityLevel == SecurityGroupType.System) { - return Task.FromResult(true); + return true; } - //there must be an acl - if (acl == null) + var appliedAcl = acl; + if (appliedAcl == null) { - return Task.FromResult(false); + appliedAcl = (await driveManager.GetDriveAsync(driveId)).DefaultReadAcl; + + //there must be an acl + if (appliedAcl == null) + { + return false; + } } //if file has required circles, see if caller has at least one - var requiredCircles = acl.GetRequiredCircles().ToList(); + var requiredCircles = appliedAcl.GetRequiredCircles().ToList(); if (requiredCircles.Any() && !requiredCircles.Intersect(caller!.Circles.Select(c => c.Value)).Any()) { - return Task.FromResult(false); + return false; } - if (acl.GetRequiredIdentities().Any()) + if (appliedAcl.GetRequiredIdentities().Any()) { throw new NotImplementedException("TODO: enforce logic for required identities"); } - switch (acl.RequiredSecurityGroup) + switch (appliedAcl.RequiredSecurityGroup) { case SecurityGroupType.Anonymous: - return Task.FromResult(true); + return true; case SecurityGroupType.Authenticated: - return Task.FromResult(((int)caller!.SecurityLevel) >= (int)SecurityGroupType.Authenticated); + return ((int)caller!.SecurityLevel) >= (int)SecurityGroupType.Authenticated; case SecurityGroupType.AutoConnected: case SecurityGroupType.Connected: - return CallerIsConnected(odinContext); + return await CallerIsConnected(odinContext); } - return Task.FromResult(false); + return false; } private void ThrowWhenFalse(bool eval) diff --git a/src/services/Odin.Services/Authorization/Acl/IDriveAclAuthorizationService.cs b/src/services/Odin.Services/Authorization/Acl/IDriveAclAuthorizationService.cs index 4a059d0f62..2a3e7ca6a1 100644 --- a/src/services/Odin.Services/Authorization/Acl/IDriveAclAuthorizationService.cs +++ b/src/services/Odin.Services/Authorization/Acl/IDriveAclAuthorizationService.cs @@ -1,17 +1,16 @@ -using System.Collections.Generic; +using System; using System.Threading.Tasks; using Odin.Core.Identity; -using Odin.Core.Storage.SQLite; using Odin.Services.Base; namespace Odin.Services.Authorization.Acl { public interface IDriveAclAuthorizationService { - Task IdentityHasPermissionAsync(OdinId odinId, AccessControlList acl, IOdinContext odinContext); + Task IdentityMatchesAclAsync(Guid driveId,OdinId odinId, AccessControlList acl, IOdinContext odinContext); - Task AssertCallerHasPermission(AccessControlList acl, IOdinContext odinContext); + Task AssertCallerMatchesAclAsync(Guid driveId, AccessControlList acl, IOdinContext odinContext); - Task CallerHasPermission(AccessControlList acl, IOdinContext odinContext); + Task CallerMatchesAclAsync(Guid driveId, AccessControlList appliedAcl, IOdinContext odinContext); } } \ No newline at end of file diff --git a/src/services/Odin.Services/DataSubscription/FeedDriveDistributionRouter.cs b/src/services/Odin.Services/DataSubscription/FeedDriveDistributionRouter.cs index 920d1a9e5b..6d02b13d7e 100644 --- a/src/services/Odin.Services/DataSubscription/FeedDriveDistributionRouter.cs +++ b/src/services/Odin.Services/DataSubscription/FeedDriveDistributionRouter.cs @@ -309,7 +309,8 @@ private async Task SendFileOverTransit(ServerFileHeader header, List rec else { // this should not happen - _logger.LogError("No transfer status found for recipient [{recipient}] for fileId [{fileId}] on [{drive}]", recipient, file.FileId, + _logger.LogError("No transfer status found for recipient [{recipient}] for fileId [{fileId}] on [{drive}]", recipient, + file.FileId, file.DriveId); } } @@ -374,10 +375,11 @@ private async Task> GetConnectedFollowersWithFilePermissionAsync(ID { return []; } - + // find all followers that are connected, return those which are not to be processed differently - var connectedIdentities = await _circleNetworkService.GetCircleMembersAsync(SystemCircleConstants.ConfirmedConnectionsCircleId, odinContext); - + var connectedIdentities = + await _circleNetworkService.GetCircleMembersAsync(SystemCircleConstants.ConfirmedConnectionsCircleId, odinContext); + // NOTE! // // ChatGPT has refactored the original code below to run asynchronously. @@ -390,7 +392,7 @@ private async Task> GetConnectedFollowersWithFilePermissionAsync(ID // db) // .GetAwaiter().GetResult()).ToList(); // return connectedFollowers; - + // // ChatGPT from here: // @@ -402,7 +404,8 @@ private async Task> GetConnectedFollowersWithFilePermissionAsync(ID var permissionTasks = intersectedFollowers.Select(async follower => new { OdinId = (OdinId)follower.DomainName, - HasPermission = await _driveAcl.IdentityHasPermissionAsync( + HasPermission = await _driveAcl.IdentityMatchesAclAsync( + notification.ServerFileHeader.FileMetadata.File.DriveId, (OdinId)follower.DomainName, notification.ServerFileHeader.ServerMetadata.AccessControlList, odinContext) diff --git a/src/services/Odin.Services/Drives/FileSystem/Base/DriveStorageServiceBase.cs b/src/services/Odin.Services/Drives/FileSystem/Base/DriveStorageServiceBase.cs index 24c1875afe..e1c4ffcd3e 100644 --- a/src/services/Odin.Services/Drives/FileSystem/Base/DriveStorageServiceBase.cs +++ b/src/services/Odin.Services/Drives/FileSystem/Base/DriveStorageServiceBase.cs @@ -158,7 +158,6 @@ await mediator.Publish(new DriveFileChangedNotification /// Writes a new file header w/o checking for an existing one /// public async Task WriteNewFileHeader(InternalDriveFileId targetFile, ServerFileHeader header, IOdinContext odinContext, - bool raiseEvent = false) { if (!header.IsValid()) @@ -239,7 +238,8 @@ public async Task DeleteTempFiles(InternalDriveFileId file, IOdinContext odinCon await tempStorageManager.EnsureDeleted(drive, file.FileId); } - public async Task<(Stream stream, ThumbnailDescriptor thumbnail)> GetThumbnailPayloadStreamAsync(InternalDriveFileId file, int width, + public async Task<(Stream stream, ThumbnailDescriptor thumbnail)> GetThumbnailPayloadStreamAsync(InternalDriveFileId file, + int width, int height, string payloadKey, UnixTimeUtcUnique payloadUid, IOdinContext odinContext, bool directMatchOnly = false) { @@ -364,7 +364,8 @@ public async Task CallerHasPermissionToFile(InternalDriveFileId file, IOdi return false; } - return await driveAclAuthorizationService.CallerHasPermission(header.ServerMetadata.AccessControlList, odinContext); + return await driveAclAuthorizationService.CallerMatchesAclAsync(file.DriveId, header.ServerMetadata.AccessControlList, + odinContext); } public async Task GetServerFileHeader(InternalDriveFileId file, IOdinContext odinContext) @@ -412,7 +413,8 @@ public async Task ResolveFileSystemType(InternalDriveFileId file return header.ServerMetadata.FileSystemType; } - public async Task GetPayloadStreamAsync(InternalDriveFileId file, string key, FileChunk chunk, IOdinContext odinContext) + public async Task GetPayloadStreamAsync(InternalDriveFileId file, string key, FileChunk chunk, + IOdinContext odinContext) { await AssertCanReadDriveAsync(file.DriveId, odinContext); DriveFileUtility.AssertValidPayloadKey(key); @@ -515,7 +517,8 @@ public async Task CommitNewFile(InternalDriveFileId targetFile, KeyHeader keyHea var extension = DriveFileUtility.GetThumbnailFileExtension(descriptor.Key, descriptor.Uid, thumb.PixelWidth, thumb.PixelHeight); var sourceThumbnail = await tempStorageManager.GetPath(drive, targetFile.FileId, extension); - await longTermStorageManager.MoveThumbnailToLongTermAsync(drive, targetFile.FileId, sourceThumbnail, descriptor, thumb); + await longTermStorageManager.MoveThumbnailToLongTermAsync(drive, targetFile.FileId, sourceThumbnail, descriptor, + thumb); } } } @@ -604,7 +607,8 @@ public async Task OverwriteFile(InternalDriveFileId tempFile, InternalDriveFileI var extension = DriveFileUtility.GetThumbnailFileExtension(descriptor.Key, descriptor.Uid, thumb.PixelWidth, thumb.PixelHeight); var sourceThumbnail = await tempStorageManager.GetPath(drive, tempFile.FileId, extension); - await longTermStorageManager.MoveThumbnailToLongTermAsync(drive, targetFile.FileId, sourceThumbnail, descriptor, thumb); + await longTermStorageManager.MoveThumbnailToLongTermAsync(drive, targetFile.FileId, sourceThumbnail, descriptor, + thumb); } } } @@ -848,7 +852,6 @@ public async Task WriteNewFileToFeedDriveAsync(KeyHeader keyHeader, FileMetadata } public async Task ReplaceFileMetadataOnFeedDrive(InternalDriveFileId file, FileMetadata fileMetadata, IOdinContext odinContext, - bool bypassCallerCheck = false) { await AssertCanWriteToDrive(file.DriveId, odinContext); @@ -963,7 +966,6 @@ await mediator.Publish(new ReactionPreviewUpdatedNotification } public async Task UpdateActiveFileHeader(InternalDriveFileId targetFile, ServerFileHeader header, IOdinContext odinContext, - bool raiseEvent = false) { await UpdateActiveFileHeaderInternal(targetFile, header, false, odinContext, raiseEvent); @@ -1030,7 +1032,8 @@ public async Task UpdateBatchAsync(InternalDriveFileId tempFile, InternalDriveFi var extension = DriveFileUtility.GetThumbnailFileExtension(newDescriptor.Key, newDescriptor.Uid, thumb.PixelWidth, thumb.PixelHeight); var sourceThumbnail = await tempStorageManager.GetPath(drive, tempFile.FileId, extension); - await longTermStorageManager.MoveThumbnailToLongTermAsync(drive, targetFile.FileId, sourceThumbnail, newDescriptor, thumb); + await longTermStorageManager.MoveThumbnailToLongTermAsync(drive, targetFile.FileId, sourceThumbnail, newDescriptor, + thumb); } // @@ -1081,6 +1084,7 @@ await mediator.Publish(new DriveFileChangedNotification //); } + private async Task WriteFileHeaderInternal(ServerFileHeader header, bool keepSameVersionTag = false) { if (!keepSameVersionTag) @@ -1189,7 +1193,8 @@ private async Task GetServerFileHeaderInternal(InternalDriveFi return null; } - await driveAclAuthorizationService.AssertCallerHasPermission(header.ServerMetadata.AccessControlList, odinContext); + await driveAclAuthorizationService.AssertCallerMatchesAclAsync(file.DriveId, header.ServerMetadata.AccessControlList, + odinContext); return header; } @@ -1272,7 +1277,8 @@ private async Task DeletePayloadFromDiskInternal(InternalDriveFileId file, Paylo // Delete the thumbnail files for this payload foreach (var thumb in descriptor.Thumbnails ?? new List()) { - await longTermStorageManager.DeleteThumbnailFile(drive, file.FileId, descriptor.Key, descriptor.Uid, thumb.PixelWidth, thumb.PixelHeight); + await longTermStorageManager.DeleteThumbnailFile(drive, file.FileId, descriptor.Key, descriptor.Uid, thumb.PixelWidth, + thumb.PixelHeight); } // Delete the payload file diff --git a/src/services/Odin.Services/Drives/Management/CreateDriveRequest.cs b/src/services/Odin.Services/Drives/Management/CreateDriveRequest.cs index 1cc27fdace..9c919246a5 100644 --- a/src/services/Odin.Services/Drives/Management/CreateDriveRequest.cs +++ b/src/services/Odin.Services/Drives/Management/CreateDriveRequest.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Odin.Services.Authorization.Acl; namespace Odin.Services.Drives.Management; @@ -12,5 +13,7 @@ public class CreateDriveRequest public bool AllowSubscriptions { get; set; } public bool OwnerOnly { get; set; } + public AccessControlList DefaultReadAcl { get; set; } + public Dictionary Attributes { get; set; } } \ No newline at end of file diff --git a/src/services/Odin.Services/Drives/Management/DriveManager.cs b/src/services/Odin.Services/Drives/Management/DriveManager.cs index 560e644449..42d88b2417 100644 --- a/src/services/Odin.Services/Drives/Management/DriveManager.cs +++ b/src/services/Odin.Services/Drives/Management/DriveManager.cs @@ -5,12 +5,12 @@ using System.Threading.Tasks; using MediatR; using Microsoft.Extensions.Logging; -using Nito.AsyncEx; using Odin.Core; using Odin.Core.Cryptography.Crypto; using Odin.Core.Cryptography.Data; using Odin.Core.Exceptions; using Odin.Core.Storage; +using Odin.Services.Authorization.Acl; using Odin.Core.Storage.Database.Identity.Table; using Odin.Core.Util; using Odin.Services.Base; @@ -107,7 +107,8 @@ public async Task CreateDriveAsync(CreateDriveRequest request, IOd AllowAnonymousReads = request.AllowAnonymousReads, AllowSubscriptions = request.AllowSubscriptions, OwnerOnly = request.OwnerOnly, - Attributes = request.Attributes + Attributes = request.Attributes, + DefaultReadAcl = request.DefaultReadAcl }; storageKey.Wipe(); @@ -165,6 +166,18 @@ await _mediator.Publish(new DriveDefinitionAddedNotification } } + public async Task UpdateDefaultReadAclAsync(Guid driveId, AccessControlList readAcl, IOdinContext odinContext) + { + odinContext.Caller.AssertHasMasterKey(); + + var sdb = await DriveStorage.GetAsync(_tblKeyThreeValue, driveId); + sdb.DefaultReadAcl = readAcl; + await DriveStorage.UpsertAsync(_tblKeyThreeValue, driveId, sdb.TargetDriveInfo.ToKey(), DriveDataType, sdb); + + CacheDrive(ToStorageDrive(sdb)); + } + + public async Task UpdateMetadataAsync(Guid driveId, string metadata, IOdinContext odinContext) { odinContext.Caller.AssertHasMasterKey(); @@ -216,7 +229,7 @@ public async Task GetDriveAsync(TargetDrive targetDrive, bool fail var driveId = await GetDriveIdByAliasAsync(targetDrive, failIfInvalid); return await GetDriveAsync(driveId.GetValueOrDefault(), failIfInvalid); } - + public async Task GetDriveIdByAliasAsync(TargetDrive targetDrive, bool failIfInvalid = false) { var cachedDrive = _driveCache.SingleOrDefault(d => d.Value.TargetDriveInfo == targetDrive).Value; @@ -283,7 +296,8 @@ public async Task> GetAnonymousDrivesAsync(PageOptions // - private async Task> GetDrivesInternalAsync(bool enforceSecurity, PageOptions pageOptions, IOdinContext odinContext) + private async Task> GetDrivesInternalAsync(bool enforceSecurity, PageOptions pageOptions, + IOdinContext odinContext) { List allDrives; diff --git a/src/services/Odin.Services/Drives/StorageDrive.cs b/src/services/Odin.Services/Drives/StorageDrive.cs index fae9adca1f..739bb35cfb 100644 --- a/src/services/Odin.Services/Drives/StorageDrive.cs +++ b/src/services/Odin.Services/Drives/StorageDrive.cs @@ -6,6 +6,7 @@ using Odin.Core.Cryptography.Crypto; using Odin.Core.Cryptography.Data; using Odin.Core.Exceptions; +using Odin.Services.Authorization.Acl; using Odin.Services.Drives.DriveCore.Storage; namespace Odin.Services.Drives @@ -107,6 +108,11 @@ public override bool OwnerOnly set { } } + public override AccessControlList DefaultReadAcl { + get => _inner.DefaultReadAcl; + set { _inner.DefaultReadAcl = value; } + } + public string GetLongTermPayloadStoragePath() { return Path.Combine(_longTermPayloadPath, "files"); @@ -195,5 +201,10 @@ public class StorageDriveBase public virtual bool AllowSubscriptions { get; set; } public virtual Dictionary Attributes { get; set; } + + /// + /// A default for reading files when the file has a null acl + /// + public virtual AccessControlList DefaultReadAcl { get; set; } } } \ No newline at end of file diff --git a/src/services/Odin.Services/Peer/Outgoing/Drive/Transfer/Outbox/Files/SendUnencryptedFeedFileOutboxWorkerAsync.cs b/src/services/Odin.Services/Peer/Outgoing/Drive/Transfer/Outbox/Files/SendUnencryptedFeedFileOutboxWorkerAsync.cs index b741e9a350..a04e44f57a 100644 --- a/src/services/Odin.Services/Peer/Outgoing/Drive/Transfer/Outbox/Files/SendUnencryptedFeedFileOutboxWorkerAsync.cs +++ b/src/services/Odin.Services/Peer/Outgoing/Drive/Transfer/Outbox/Files/SendUnencryptedFeedFileOutboxWorkerAsync.cs @@ -8,7 +8,6 @@ using Odin.Core.Identity; using Odin.Core.Serialization; using Odin.Core.Storage; -using Odin.Core.Storage.SQLite; using Odin.Core.Time; using Odin.Core.Util; using Odin.Services.Authorization.Acl; @@ -90,7 +89,7 @@ IDriveAclAuthorizationService driveAcl var versionTag = header.FileMetadata.VersionTag; var globalTransitId = header.FileMetadata.GlobalTransitId; - var authorized = await driveAcl.IdentityHasPermissionAsync(recipient, + var authorized = await driveAcl.IdentityMatchesAclAsync(file.DriveId, recipient, header.ServerMetadata.AccessControlList, odinContext); if (!authorized) @@ -155,7 +154,8 @@ IDriveAclAuthorizationService driveAcl } } - private async Task> SendFile(ServerFileHeader header, FeedDistributionItem distroItem, OdinId recipient, + private async Task> SendFile(ServerFileHeader header, FeedDistributionItem distroItem, + OdinId recipient, CancellationToken cancellationToken) { var request = new UpdateFeedFileMetadataRequest() @@ -183,7 +183,8 @@ await TryRetry.WithDelayAsync( return httpResponse; } - private async Task> DeleteFile(ServerFileHeader header, OdinId recipient, FileSystemType fileSystemType, + private async Task> DeleteFile(ServerFileHeader header, OdinId recipient, + FileSystemType fileSystemType, CancellationToken cancellationToken) { var request = new DeleteFeedFileMetadataRequest()