feat: let apps use another app's data with user consent - #3516
Conversation
|
@Salazareo Following up on our conversation about reusing the ACL to manage file system permissions because of the existing rules, I took another look at it. After reviewing the implementation, I decided it's better to keep the logic independent in
|
also adds the same exclusion for the batchPut api, small change
Salazareo
left a comment
There was a problem hiding this comment.
gave it a test, and was mostly good, one small thing we missed was the app access tokens, but thats on me, we didn't have that well explained in the code or actor object, have added that and updated some adjacent code.
also fixed a small bug with the batchPut, and some sql sanitation for the perm which likely wouldn't have been an issue, but wasn't too signficant
Lets one app use another app's per-user state — that app's KV namespace and its
/<user>/AppData/<appUid>directory — after the user consents. A calendar can read a contacts app's entries to show birthdays, add an invite, and cancel it again if the user allowed that.Permission model
One new namespace,
app-data:<targetAppUid>:<store>:<op>, wherestoreiskvorfs. Grants are ordinaryuser_to_app_permissionsrows written by the existing/auth/grant-user-app, so consent, audit, revoke and cacheinvalidation all come for free. No new tables, no migration.
Three classes per store:
read,write, anddelete.deleteis orthogonal towrite— neither implies the other. That is what makes "may add invites but not remove them" expressible, and it is pinned by a test so a latersimplification can't quietly fold them together. Coarser grants (
app-data:X:kv,app-data:X) still cover all three through the existing prefix implication.Security callout
Flagging this per AGENTS.md, since it is a permission change.
app-data:has nomanage:form, andcanManagePermissiongatesgrantUserUserPermissionandgrantDevAppPermission— so a developer cannot pre-authorise their own appfor every user, and a user cannot pass the access on to another user.
kv:flushmaps tonulland is refused before any lookup, at every scope includingapp-data:<uid>.set/batchPutacceptexpireAtandupdateacceptsttl; a past value makes a key vanish. Cross-app calls carrying either additionally require thedeleteclass, otherwisekv:setalone would be a delete capability under another name.#scanUserAppstill resolves every row through the issuing user.Behaviour change
An app passing a foreign
appUuidtoputer.kv.*previously had it silently dropped and read its own namespace. It now reads the target's data with a grant, or gets a 403 (404 if the target app does not exist). Silently answering a different question than the caller asked was the worse failure, but it is a change — the test that asserted the old behaviour is rewritten rather than deleted.Where it is enforced
AppPermissionServiceAppPermissionServiceKVStoreDriver.#opts+SystemKVStore.getNamespaceapp-owns-appdataimplicator inFSServiceFSService.remove/move/renameAuthControllerapp.changedlistener +PermissionStoreprefix sweepFS deletion is enforced in
FSServicerather than ACL because delete, move and rename all ask ACL forfs:write— ACL structurally cannot tell them apart, and adding adeletemode would force a platform-wide answer to "does write imply delete" that breaks either this feature or existing grants.FSServiceis also the one choke point both FS controllers and the/batchdispatcher pass through.Per-entry privacy
Consent is coarse: a user allowing "Contacts' data" cannot see that the namespace holds an OAuth token. So an app can mark entries private when it writes them:
A private entry is invisible and untouchable to any other app — reads return null, listings omit it, writes and deletes are refused — regardless of what the user granted. Stored as a reserved DynamoDB item attribute beside
value/ttl,so no table change.
listfilters inside the query rather than after, soincludeTotalcannot leak what the flag hides.An app can also opt out entirely with
share_app_data: falsein its metadata, which is the only lever for secrets written before this shipped.Withdrawal
The target app's uid lives inside the permission string, not a column, so no foreign key can cascade it. Grants are swept when the target is deleted, when its uid is reused by a new app (origin-derived uids are deterministic uuidv5 and regenerate verbatim), and when it stops sharing. Each removal is audited and the holder's permission cache generation bumped, so it takes effect immediately rather than after the 20s scan TTL.
Frontend
puter.perms.requestAppData()builds the scopes and prompts; IPC and the popup transport carry a list; the consent dialog renders one row per scope, names the target app rather than a uid, and refuses the whole prompt if any single scope is undescribable — a scope the user never saw must not ride along on an Allow. Deletion is named explicitly in the copy, including for coarse scopes that imply it.Testing
readnot carrying write,delrefused without the delete class,flushrefused at every scope, expiry refused withoutdelete, a private entry hidden from a granted app but visible to its owner,mkdir-on-grant, and cascade-revoke taking effect on the very next read with a warm cache.The GUI click-through path has been exercised manually; the third-party popup transport has not.
Known gaps and follow-ups
disableSharingis honoured byset()only;update/addignore it.operation window, and the flag is only ever set by the owner.
distinguishable. App uids are already discoverable via
puter.apps.get, andchecking permission first would not close it.
app-data:rows from/auth/list-permissionsand revoke each.en.jsonly.npm run check-translationsis brokenrepo-wide (both
package.jsonfiles point at a missing script).shareable key prefix or subdirectory and no grant can exceed it.