diff --git a/.lycheeignore b/.lycheeignore index 49be24e1..4a7c4d8b 100644 --- a/.lycheeignore +++ b/.lycheeignore @@ -79,12 +79,14 @@ https://securetoken.google.com/ file:///home/runner/work/solidui/solidui/lib/src/utils/scheme https://pods.example.au/john-doe/profile/card#me +https://anushkavidanage.github.io/solidpod/example/client-profile.jsonld +https://anushkavidanage.github.io/solidpod/example/redirect.html +https://anusii.github.io/solidpodeg/client-profile.jsonld https://github.com/gjwgit/myapp/blob/main/README.md https://github.com/anusii/solidpod/blob/main/solidpodeg/README.md https://server/POD_NAME/APP_NAME/data/FILE_PATH https://server/alice/ https://server/alice/myapp/ -https://anusii.github.io/solidpodeg/client-profile.jsonld # 20260605 gjw Failing solid servers @@ -97,4 +99,4 @@ https://api.open-meteo.com/v1/forecast # 20260619 gjw konapod failures -https://blog.kumo.dev/2024/05/22/reverse_engineering_hkg_apps.html \ No newline at end of file +https://blog.kumo.dev/2024/05/22/reverse_engineering_hkg_apps.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ee9ba49..5cb77bdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Visit the package at [pub.dev](https://pub.dev/packages/solidpod). ## 1.0 ++ Switch public to private sharing [1.0.17 20260811 jesscmoore] + Preserve encryption state on overwrite [1.0.16 20260811 jesscmoore] + Fix deleting a large file stored as a single chunk [1.0.15 20260809 gjw] + Remove debug output when reading a large file [1.0.14 20260809 gjw] diff --git a/lib/src/solid/grant_permission.dart b/lib/src/solid/grant_permission.dart index 4b868b70..68f68022 100644 --- a/lib/src/solid/grant_permission.dart +++ b/lib/src/solid/grant_permission.dart @@ -38,6 +38,10 @@ import 'package:solidpod/src/solid/api/rest_api.dart'; import 'package:solidpod/src/solid/constants/common.dart'; import 'package:solidpod/src/solid/constants/web_acl.dart'; import 'package:solidpod/src/solid/models/log_entry.dart'; +import 'package:solidpod/src/solid/read_permission.dart' + show getUserClassPermissions; +import 'package:solidpod/src/solid/revoke_permission.dart' + show revokePermission; import 'package:solidpod/src/solid/solid_func_call_status.dart'; import 'package:solidpod/src/solid/utils/exceptions.dart'; import 'package:solidpod/src/solid/utils/get_url_helper.dart'; @@ -70,6 +74,17 @@ import 'package:solidpod/src/solid/utils/permission.dart' show genAclTurtle; /// - [isFile] Optional flag describing whether the resources is a file or /// not. /// - [groupName] - Optional name of the group permission. +/// - [revokePublicAccessOnSpecificGrant] - When [recipientType] is +/// individual or group and the resource currently has a Public or +/// Authenticated User class grant (and is therefore plaintext on the +/// server, per the decryption step this function performs for those +/// recipient classes), revoke that grant and re-encrypt the resource +/// before proceeding. Without this, the resource would end up both still +/// readable by the previously-granted class *and* holding a stale +/// individual key that does not match the (still plaintext) content. +/// Defaults to `true`; set to `false` to keep today's behaviour where +/// granting to a specific recipient never touches an existing +/// public/authUser grant. Future grantPermission({ required String fileName, @@ -81,6 +96,7 @@ Future grantPermission({ bool isFile = true, bool isExternalRes = false, String? groupName, + bool revokePublicAccessOnSpecificGrant = true, }) async { if (!await isUserLoggedIn()) { throw NotLoggedInException( @@ -150,6 +166,45 @@ Future grantPermission({ // if recipient pods have been initialised if (allRecipientsInitialised || !hasSpecificRecipients) { if (resStatus == ResourceStatus.exist) { + // Sharing to a specific individual/group assumes the resource is + // ciphertext under an individual key (see the `fileHasIndKey` + // branch below). If it's currently also granted to the Public or + // Authenticated User class, it's plaintext on the server (that + // class has no key of its own to decrypt with) — so revoke that + // grant and re-encrypt first. Otherwise the resource would end up + // both still openly readable *and* holding a stale individual key + // that doesn't match the (still plaintext) bytes. Must run before + // `setPermissionAcl` below, so `revokePermission`'s own ACL read + // still sees the pre-existing grant. + if (hasSpecificRecipients && revokePublicAccessOnSpecificGrant) { + final existingClassPerms = await getUserClassPermissions( + fileName: resourceUrl, + isFile: isFile, + isFileUrl: true, + isExternalRes: isExternalRes, + ); + for (final classType in existingClassPerms.keys) { + final classAgent = classType == RecipientType.public + ? publicAgent + : authenticatedAgent; + debugPrint( + '[grantPermission] revoking existing $classType access on ' + '"$resourceUrl" before granting to $recipientType', + ); + await revokePermission( + fileName: resourceUrl, + isFileUrl: true, + permissionList: existingClassPerms[classType]!, + recipientIndOrGroupWebId: classAgent.value, + recipientType: classType, + ownerWebId: ownerWebId, + granterWebId: granterWebId, + isFile: isFile, + isExternalRes: isExternalRes, + ); + } + } + // Add the permission line to the relevant ACL file await setPermissionAcl( resourceUrl, diff --git a/lib/src/solid/read_permission.dart b/lib/src/solid/read_permission.dart index 70f8eb50..16bcc76d 100644 --- a/lib/src/solid/read_permission.dart +++ b/lib/src/solid/read_permission.dart @@ -30,6 +30,12 @@ library; import 'dart:core'; +import 'package:rdflib/rdflib.dart'; + +import 'package:solidpod/src/solid/constants/common.dart' + show agentClassPred, agentStr, permStr; +import 'package:solidpod/src/solid/constants/web_acl.dart' + show RecipientType, authenticatedAgent, publicAgent; import 'package:solidpod/src/solid/utils/get_url_helper.dart'; import 'package:solidpod/src/solid/utils/misc.dart'; import 'package:solidpod/src/solid/utils/permission.dart'; @@ -70,3 +76,41 @@ Future> readPermission({ return permMap; } + +/// The Public/Authenticated-User access modes currently granted on +/// [fileName], keyed by [RecipientType.public]/[RecipientType.authUser]. +/// A class with no current grant is omitted from the result. +/// +/// Single source of truth for "does this resource currently have a +/// Public/AuthenticatedUser grant" — used both by [grantPermission] (to +/// decide whether an individual/group grant must first revoke and +/// re-encrypt) and by solidui's confirmation dialog for the same action. + +Future>> getUserClassPermissions({ + required String fileName, + required bool isFile, + bool isFileUrl = false, + bool isExternalRes = false, +}) async { + final permMap = await readPermission( + fileName: fileName, + isFile: isFile, + isFileUrl: isFileUrl, + isExternalRes: isExternalRes, + ); + + final result = >{}; + for (final receiverId in permMap.keys) { + if (receiverId is! String || + permMap[receiverId][agentStr] != agentClassPred) { + continue; + } + final perms = (permMap[receiverId][permStr] as List).cast(); + if (URIRef(receiverId) == publicAgent) { + result[RecipientType.public] = perms; + } else if (URIRef(receiverId) == authenticatedAgent) { + result[RecipientType.authUser] = perms; + } + } + return result; +} diff --git a/pubspec.yaml b/pubspec.yaml index 2197a788..1f449063 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: solidpod description: Support access to private data from PODs on Solid servers. -version: 1.0.16 +version: 1.0.17 homepage: https://github.com/anusii/solidpod environment: