From 2e80a5b3f4b5b8de8b5f52a803d51a0ff2745d05 Mon Sep 17 00:00:00 2001 From: toddmitchell Date: Fri, 29 Nov 2024 17:27:20 -0800 Subject: [PATCH] basic start to adding default acl support --- .../Drive/OwnerDriveManagementController.cs | 13 ++++ .../Acl/DriveAclAuthorizationService.cs | 68 ++++++++++++------- .../Acl/IDriveAclAuthorizationService.cs | 12 ++-- .../FeedDriveDistributionRouter.cs | 33 +++++---- .../Base/DriveStorageServiceBase.cs | 14 ++-- .../Drives/Management/CreateDriveRequest.cs | 3 + .../Drives/Management/DriveManager.cs | 16 ++++- .../Odin.Services/Drives/StorageDrive.cs | 11 +++ ...endUnencryptedFeedFileOutboxWorkerAsync.cs | 11 +-- 9 files changed, 129 insertions(+), 52 deletions(-) diff --git a/src/apps/Odin.Hosting/Controllers/OwnerToken/Drive/OwnerDriveManagementController.cs b/src/apps/Odin.Hosting/Controllers/OwnerToken/Drive/OwnerDriveManagementController.cs index aa70324a6f..9f63fcd7f9 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,15 @@ public async Task UpdateDriveMetadata([FromBody] UpdateDriveDefinitionRequ return true; } + [HttpPost("update-default-read-acl")] + public async Task UpdateDefaultReadAclAsync([FromBody] UpdateDriveDefinitionRequest request) + { + var db = _tenantSystemStorage.IdentityDatabase; + var driveId = await _driveManager.GetDriveIdByAliasAsync(request.TargetDrive, db, true); + await _driveManager.UpdateDefaultReadAclAsync(driveId.GetValueOrDefault(), request.DefaultReadAcl, WebOdinContext, db); + return true; + } + [HttpPost("UpdateAttributes")] public async Task UpdateDriveAttributes([FromBody] UpdateDriveDefinitionRequest request) { @@ -119,6 +130,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 44fa227950..e55b947a6e 100644 --- a/src/services/Odin.Services/Authorization/Acl/DriveAclAuthorizationService.cs +++ b/src/services/Odin.Services/Authorization/Acl/DriveAclAuthorizationService.cs @@ -6,30 +6,43 @@ using Odin.Core.Identity; using Odin.Core.Storage.SQLite.IdentityDatabase; 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, IdentityDatabase db, IOdinContext odinContext) { - ThrowWhenFalse(await CallerHasPermission(acl, odinContext)); + ThrowWhenFalse(await CallerMatchesAclAsync(driveId, acl, db, odinContext)); } - public async Task IdentityHasPermissionAsync(OdinId odinId, AccessControlList acl, IOdinContext odinContext, IdentityDatabase db) + public async Task IdentityMatchesAclAsync(Guid driveId, + OdinId odinId, + AccessControlList acl, + IOdinContext odinContext, + IdentityDatabase db) { - //there must be an acl - if (acl == null) + var appliedAcl = acl; + + if (appliedAcl == null) { - return false; + appliedAcl = (await driveManager.GetDriveAsync(driveId, db)).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); @@ -42,17 +55,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; @@ -64,51 +78,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, IdentityDatabase db, 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, db)).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 7a198dd911..7e86aabbc4 100644 --- a/src/services/Odin.Services/Authorization/Acl/IDriveAclAuthorizationService.cs +++ b/src/services/Odin.Services/Authorization/Acl/IDriveAclAuthorizationService.cs @@ -1,7 +1,6 @@ -using System.Collections.Generic; +using System; using System.Threading.Tasks; using Odin.Core.Identity; -using Odin.Core.Storage.SQLite; using Odin.Core.Storage.SQLite.IdentityDatabase; using Odin.Services.Base; @@ -9,10 +8,11 @@ namespace Odin.Services.Authorization.Acl { public interface IDriveAclAuthorizationService { - Task IdentityHasPermissionAsync(OdinId odinId, AccessControlList acl, IOdinContext odinContext, IdentityDatabase db); - - Task AssertCallerHasPermission(AccessControlList acl, IOdinContext odinContext); + Task IdentityMatchesAclAsync(Guid driveId, OdinId odinId, AccessControlList appliedAcl, IOdinContext odinContext, + IdentityDatabase db); - Task CallerHasPermission(AccessControlList acl, IOdinContext odinContext); + Task AssertCallerMatchesAclAsync(Guid driveId, AccessControlList acl, IdentityDatabase db, IOdinContext odinContext); + + Task CallerMatchesAclAsync(Guid driveId, AccessControlList appliedAcl, IdentityDatabase db, 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 26600b2809..e79e456341 100644 --- a/src/services/Odin.Services/DataSubscription/FeedDriveDistributionRouter.cs +++ b/src/services/Odin.Services/DataSubscription/FeedDriveDistributionRouter.cs @@ -132,7 +132,8 @@ public async Task Handle(IDriveNotification notification, CancellationToken canc } } - private async Task EnqueueFileMetadataNotificationForDistributionUsingFeedEndpoint(IDriveNotification notification, IdentityDatabase db) + private async Task EnqueueFileMetadataNotificationForDistributionUsingFeedEndpoint(IDriveNotification notification, + IdentityDatabase db) { var item = new FeedDistributionItem() { @@ -192,7 +193,8 @@ private async Task ShouldDistribute(IDriveNotification notification, bool return true; } - private async Task DistributeToCollaborativeChannelMembers(IDriveNotification notification, IOdinContext odinContext, IdentityDatabase db) + private async Task DistributeToCollaborativeChannelMembers(IDriveNotification notification, IOdinContext odinContext, + IdentityDatabase db) { var header = notification.ServerFileHeader; @@ -237,7 +239,8 @@ private async Task DistributeToCollaborativeChannelMembers(IDriveNotification no /// Distributes to connected identities that are followers using /// transit; returns the list of unconnected identities /// - private async Task DistributeToConnectedFollowersUsingTransit(IDriveNotification notification, IOdinContext odinContext, IdentityDatabase db) + private async Task DistributeToConnectedFollowersUsingTransit(IDriveNotification notification, IOdinContext odinContext, + IdentityDatabase db) { var connectedFollowers = await GetConnectedFollowersWithFilePermissionAsync(notification, odinContext, db); if (connectedFollowers.Any()) @@ -277,7 +280,8 @@ private async Task> GetFollowersAsync(Guid driveId, IOdinContext od return recipients; } - private async Task SendFileOverTransit(ServerFileHeader header, List recipients, IOdinContext odinContext, IdentityDatabase db) + private async Task SendFileOverTransit(ServerFileHeader header, List recipients, IOdinContext odinContext, + IdentityDatabase db) { var file = header.FileMetadata.File; @@ -311,13 +315,15 @@ 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); } } } - private async Task DeleteFileOverTransit(ServerFileHeader header, List recipients, IOdinContext odinContext, IdentityDatabase db) + private async Task DeleteFileOverTransit(ServerFileHeader header, List recipients, IOdinContext odinContext, + IdentityDatabase db) { if (header.FileMetadata.GlobalTransitId.HasValue) { @@ -370,7 +376,8 @@ private async Task AddToFeedOutbox(OdinId recipient, FeedDistributionItem distro await _peerOutbox.AddItemAsync(item, useUpsert: true); } - private async Task> GetConnectedFollowersWithFilePermissionAsync(IDriveNotification notification, IOdinContext odinContext, + private async Task> GetConnectedFollowersWithFilePermissionAsync(IDriveNotification notification, + IOdinContext odinContext, IdentityDatabase db) { var followers = await GetFollowersAsync(notification.File.DriveId, odinContext); @@ -378,10 +385,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. @@ -394,7 +402,7 @@ private async Task> GetConnectedFollowersWithFilePermissionAsync(ID // db) // .GetAwaiter().GetResult()).ToList(); // return connectedFollowers; - + // // ChatGPT from here: // @@ -406,7 +414,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 b2b2bfb601..2ecd680c27 100644 --- a/src/services/Odin.Services/Drives/FileSystem/Base/DriveStorageServiceBase.cs +++ b/src/services/Odin.Services/Drives/FileSystem/Base/DriveStorageServiceBase.cs @@ -247,7 +247,8 @@ public async Task DeleteTempFiles(InternalDriveFileId file, IOdinContext odinCon await tsm.EnsureDeleted(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, IdentityDatabase db, bool directMatchOnly = false) { @@ -375,7 +376,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, db, + odinContext); } public async Task GetServerFileHeader(InternalDriveFileId file, IOdinContext odinContext, IdentityDatabase db) @@ -424,7 +426,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, IdentityDatabase db) { await AssertCanReadDriveAsync(file.DriveId, odinContext, db); @@ -1123,7 +1126,7 @@ private async Task GetLongTermStorageManagerAsync(Guid d logger.LogDebug("GetLongTermStorageManager lookup - driveId: {d}", driveId); var drive = await DriveManager.GetDriveAsync(driveId, db, failIfInvalid: true); logger.LogDebug("GetLongTermStorageManager lookup drive: {d}", drive.Name); - + var manager = new LongTermStorageManager(drive, logger, driveFileReaderWriter, driveDatabaseHost, GetFileSystemType()); return manager; } @@ -1245,7 +1248,8 @@ private async Task GetServerFileHeaderInternal(InternalDriveFi return null; } - await driveAclAuthorizationService.AssertCallerHasPermission(header.ServerMetadata.AccessControlList, odinContext); + await driveAclAuthorizationService.AssertCallerMatchesAclAsync(file.DriveId, header.ServerMetadata.AccessControlList, db, + odinContext); return header; } 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 2525c1226f..95ba668dd8 100644 --- a/src/services/Odin.Services/Drives/Management/DriveManager.cs +++ b/src/services/Odin.Services/Drives/Management/DriveManager.cs @@ -13,6 +13,7 @@ using Odin.Core.Exceptions; using Odin.Core.Storage; using Odin.Core.Storage.SQLite.IdentityDatabase; +using Odin.Services.Authorization.Acl; using Odin.Services.Base; using Odin.Services.Mediator; @@ -101,7 +102,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(); @@ -160,7 +162,19 @@ await _mediator.Publish(new DriveDefinitionAddedNotification }); } } + + public async Task UpdateDefaultReadAclAsync(Guid driveId, AccessControlList readAcl, IOdinContext odinContext, IdentityDatabase db) + { + odinContext.Caller.AssertHasMasterKey(); + + var sdb = await _driveStorage.GetAsync(db, driveId); + sdb.DefaultReadAcl = readAcl; + await _driveStorage.UpsertAsync(db, driveId, sdb.TargetDriveInfo.ToKey(), _driveDataType, sdb); + CacheDrive(ToStorageDrive(sdb)); + } + + public async Task UpdateMetadataAsync(Guid driveId, string metadata, IOdinContext odinContext, IdentityDatabase db) { odinContext.Caller.AssertHasMasterKey(); 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 ba99cae547..f5bdc8391f 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 @@ -34,7 +34,8 @@ IDriveAclAuthorizationService driveAcl ) : OutboxWorkerBase(fileItem, logger, null, odinConfiguration) { - public async Task<(bool shouldMarkComplete, UnixTimeUtc nextRun)> Send(IOdinContext odinContext, IdentityDatabase db, CancellationToken cancellationToken) + public async Task<(bool shouldMarkComplete, UnixTimeUtc nextRun)> Send(IOdinContext odinContext, IdentityDatabase db, + CancellationToken cancellationToken) { try { @@ -91,7 +92,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, db); if (!authorized) @@ -156,7 +157,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() @@ -184,7 +186,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()