Skip to content
This repository has been archived by the owner on Jun 10, 2022. It is now read-only.

[SDK] Permission system #606

Merged
merged 16 commits into from
Jun 30, 2020
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions design/permissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
Permissions API
=================

To be a responsible member of the Social MR ecosystem, the MRE SDK has to prioritize the safety of its residents over
the power of this SDK. As such, I propose the addition of a permissions API, so MREs can have more "invasive" features
without compromising user safety and agency.


Permission scope
------------------

When a user approves or denies permission to an MRE, that approval/denial is scoped to the unique combination of
protocol, hostname, and port of the MRE URL, known collectively as the "origin". So when `ws://mres.altvr.com/wearahat`
requests `user-interaction`, that permission is associated with `ws://mres.altvr.com`, and all user decisions apply
equally to new connections to the same server. These user decisions may or may not be persisted indefinitely by the host app.


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the host app should decide the scope here.
I'd also recommend rights to be managed by AppId over URL, if AppId is available

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain your reasoning for hosts making the scope determination? I can see the case for AppId over URL origin, because AppIds can uniquely identify an app and not just a server. I disagree, and think server trust is all that's required since there won't be origins with apps from multiple authors, and it's the author that the user is ultimately trusting.


In reply to: 441849523 [](ancestors = 441849523)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should have worded that differently. I wanted to say that I don't think we should assume that scope goes beyond the single app, and the host app should ask the user if they would want to permit all apps, all apps by same developer, or a single app.

I, as a user, wouldn't want the assumption made about other apps for me. Just because I want to grant one app a specific permission, doesn't mean that all other apps that are deployed to the same root url should have the same permissions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So how do you define a single app, the full URL? Does it include query arguments? The more specific we get, the more nag screens the users are gonna be presented with. I think origin strikes a good balance.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, a unique app to me is the full URL but excluding query arguments.
I think the best analogy is phone apps: If I install an app on my phone and I grant a permission, it shouldn't automatically grant permission to all other apps by that publisher. But if I launch the app with a different deep link, the permissions given to the app before still apply.

Agree that there will be more nag screens. To me that's up to the host app for how to manage. I imagine "yes/no once", "yes/no for this MRE always", "yes/no for all MREs always" would be required. It could be optional to add "yes/no for all MREs on this domain" (or "by this publisher" once MRE App registry is done), but I see that as lower priority

Features requiring user permission
------------------------------------

If a permission is not requested, it is considered denied.

* `execution` (client only) - Required to connect to an MRE server. Typically granted by default, but can be revoked.
* Allow: MRE connection can be established
* Deny: MRE connection will not be established
* `user-tracking` - Grants access to a persistent user identity across sessions. Needed for things like high scores lists.
* Allow: This user will be uniquely identified to this MRE origin across sessions and instances.
* Deny: This user will be assigned a new ID every time they connect to MREs from this origin. If the `user-interaction`
permission is also denied, this client will not join a user to the session at all.
* `user-interaction` - Behaviors, exclusive actors, attachments, and dialogs.
* Allow: This user can interact with behaviors, exclusively own actors, be a target for attached actors, and can be
sent dialogs.
* Deny: Interactions with behaviors will not be sent back to the app server. Attempts to create exclusive actors
for this user will fail. Actors attached to this user will be considered unattached. Calls to `user.prompt`
will be rejected. If the `user-tracking` permission is also denied, this client will not join a user to the
session at all.
* Play sounds and video
* Large assets (hypothetical) - If an MRE wants to load more than some large amount of assets into memory
(30MB worth of memory? TBD), the client must first approve. This permission might automatically be approved/denied
by clients on behalf of users based on device capabilities.
* Movement (hypothetical) - The ability to forcibly move a user's avatar and point of view, either smoothly
or teleported.
* Microphone input (hypothetical) - The ability for users to stream their microphone input into an MRE, for voice
commands, synthesizers, chat bots, or anything else.


Permission declaration
------------------------

Apps must declare which APIs they will use in advance of any users connecting. This is done via a JSON-formatted
manifest loaded from the app's HTTP root. For example, the manifest for the URL `ws://mres.altvr.com/tests/red` should
be found at `http://mres.altvr.com/tests/red/manifest.json`. Note that this file can be served from the filesystem or
constructed on request.

The manifest must conform to the following JSON schema:

```json
{
"$schema": "http://json-schema.org/schema#",
"type": "object",
"properties": {
"name": { "type": "string" },
"author": { "type": "string" },
"permissions": {
"type": "array",
"items": {
"type": "string",
"enum": ["user-tracking", "user-interaction"]
}
},
"optionalPermissions": {
"type": "array",
"items": {
"type": "string",
"enum": ["user-tracking", "user-interaction"]
}
}
}
}
```

Permissioning flow
--------------------

Legend:

* [App] Step executed by MRE developer code
* [SDK] Step executed by MRE SDK
* [Runtime] Step executed in client-side MRE runtime
* [Host] Step executed by host application

Startup:

1. [App] During development, the app developer authors a static manifest file, or uses the WebHost API to generate one.
2. [Runtime|Host] Initializes the MRE API with a permissions manager instance that will receive new permission requests.
If permission decisions are persistent, load them into memory now. Default provided implementation does not persist.
3. [Host] The host wishes to run an MRE, so creates an IMixedRealityExtensionApp instance and calls Startup().
4. [Runtime] Downloads the manifest from the provided MRE server. If missing, assumes no permissions required.
5. [Runtime] Calls into the permission manager requesting the manifest-listed permissions.
6. [Runtime|Host] Permission manager evaluates the required and optional MRE permissions against the current set of
grants/denials for the MRE's origin, and determines if any new permission decisions need to be made by the user.
If so:
1. [Host] Present the choices to the user by whatever means the host sees fit, persist the decision if desired,
and report the result to the Runtime. Optional permissions should be presented in such a way as they can be
granted or denied individually, but required permissions should be decided as a group.
7. [Runtime] Determine if the MRE has sufficient permission to run, i.e. `execution` and all the required permissions
are granted. If not, abort connecting to the MRE.
8. [Runtime] Startup proceeds like normal, but if a `user-joined` message is sent, the user payload must include
the IDs of any permissions that were granted to this MRE by this user (other than `execution`, which is implied).


Permissions Manager
---------------------

Host apps must provide hooks for the MRE subsystem to obtain permission from users. This is provided to `MREAPI.InitializeAPI`
as an implementation of `IPermissionManager`, which has the following methods:

* `Task<Permissions> PromptForPermission(...)` - Request permissions from the user, and return a Task that resolves
with those permissions the user has granted. Takes the following arguments:
* `string appOrigin` - The origin of the MRE requesting permission, as described above.
* `string appDisplayName` - A human-readable identifier for the MRE server provided from the manifest.
* `Permissions permissionsNeeded` - A bitfield of the permissions required to run the app.
* `Permissions permissionsWanted` - A bitfield of permissions that the app can use, but are not needed to run.
Copy link
Member

@tombuMS tombuMS Jun 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is something being passed to a host app, we may want to consider passing this in a more friendly unpacked format, so that the host app can easily digest this for their UI. Otherwise the parsing of the bitfield in to actionable items and repacking has to happen as code written by each host app. If we send this as something already parsed and receive in the same unpacked manner, we can abstract away the need to deal with separating out actionable items from a bitfield. Maybe an array of Permissions enum value that can easily be iterated over to add prompts to an overall UI? #Closed

Copy link
Member

@tombuMS tombuMS Jun 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact based on this proposal, the json will already contain an array of permission strings that could be passed along as is to this call to the host app. You could just wait until the permissions task has completed before you pack this in to a bitfield of approved permissions.


In reply to: 441643214 [](ancestors = 441643214)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point: since we're sending the perms down as arrays, there's no point in repacking them. I like bitfields because of O(1) lookups, but for <32 items I suppose it doesn't matter.


In reply to: 441644095 [](ancestors = 441644095,441643214)

Copy link
Member

@tombuMS tombuMS Jun 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean still packing for lookups is fine, I was just suggesting doing that packing after the host app has gathered the approved permissions. #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure C# lets you do foreach loops on bitfields of enums. It's cool like that. Might not matter, even for enumeration. I'll play with this come implementation time.


In reply to: 441733983 [](ancestors = 441733983)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit convoluted how you have to iterate over the values of the enum, but there is a way. It is still easier at this point since the message is carrying as an array of values to just pass that array in to the dialog and receive one back.


In reply to: 441766267 [](ancestors = 441766267,441733983)

* `Permissions CurrentPermissions(...)` - Returns a bitfield of the currently granted permissions. Takes the following
arguments:
* `string appOrigin` - The origin of the MRE for which we want the permission set.

```cs
[Flags]
enum Permissions {
None = 0,
Execution = 1,
UserTracking = 2,
UserInteraction = 4,
...
}
```

The default implementation of the permissions manager will also include the following virtual methods:

* `void ReadFromStorage()` - Populate the in-memory permission database from offline storage.
* `void WriteToStorage()` - Write the "remembered" portions of the in-memory permission database to offline storage.
Copy link
Member

@tombuMS tombuMS Jun 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this storage we would be providing and interfacing with in our runtime code, since this refers to a default permission manager? Does this mean that no permission manager is required to be supplied by the host app? #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interface does not require any kind of persistent storage. But since most hosts will probably want it, the MRE namespace will also have an abstract base implementation that provides some stubs. Haven't worked out all the details here yet.


In reply to: 441648519 [](ancestors = 441648519)

Copy link
Member

@tombuMS tombuMS Jun 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. So the stubs are not for a default implementation of local storage then. They are just for optional hooks to the permission manager supplied by the host app? #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct.


In reply to: 441735090 [](ancestors = 441735090)



Old stuff
===========


Methods for acquiring permission
----------------------------------

1. **Request permission for a feature the first time it is used**
1. The application code makes an async call to an API that requires user permission.
2. The SDK detects that this is the first time that API is used for this user, so it saves the API call request
internally and sends a permission request to the user.
3. The user's client will reply with an approved or denied message. If approved, the original API call is executed.
If not, handle the rejection.
2. **Request permission for a set of features explicitly**
1. The application code uses a permissions API to set the permission requirements for the app.
2. All current and late-joining users are sent a message asking for any permissions not already granted or denied.
3. Each user will reply with an approved or denied message. If approved, the approval is saved, and all future API
requests that require that permission will be processed for this user. If denied, handle the rejection.
3. **Establish permissions during initialization**
1. Provide a list of permissions required by an app during app setup.
2. During connection handshake, the list of required permissions will be sent to the client.
3. If the client approves, initialization proceeds like normal. If not, handle the rejection.


Methods for handling permission rejection
-------------------------------------------

1. Revoke access to the denied APIs for the set of denying users.
2. All users must approve of all permissions; users that do not approve immediately leave the MRE.
3. All clients must approve of all permissions; clients that do not approve immediately disconnect from the MRE server.


Methods for presenting permission requests to users
-----------------------------------------------------

1. Dialogs
2. Settings menu