-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposed AuthProvider API #219
Conversation
6ec3ab4
to
f7f8e19
Compare
Thanks for writing this up in such detail! I think it makes sense to introduce a richer API than
Authorization specifically is complicated (as always) and whilst there are authorization decisions which sometimes involve the transport a message arrived on, I think it's generally easier to understand a system if the authorization rules are expressed in a central place. My instinct is that we should put the responsibility for authentication in the I wonder how this design (which I present as a straw man, having put much less thought into than you have put into this PR I think) sounds to you and if it conflicts with any design goals you have in mind (e.g. around implementing local first auth)? Another question I have is whether auth/authzn applies to ephemeral messages. Currently we have a little homegrown gossip protocol for forwarding ephemeral messages from e.g. a tab to a shared worker and then on to a relay sync server. Messages can be forwarded an arbitrary number of hops so it's quite likely that a peer would receive ephemeral messages for which they have no direct connection and no authentication information. Should such messages be forwarded? |
f7f8e19
to
ab521f5
Compare
Thanks @alexjg . Let's talk next week. In the meantime, I've written this overview of how the LocalFirstAuthProvider currently works, for what it's worth: |
363f7ee
to
9de406d
Compare
ab521f5
to
32a32ae
Compare
Current dependencies on/for this PR:
This stack of pull requests is managed by Graphite. |
9de406d
to
a592683
Compare
32a32ae
to
27ef995
Compare
OK @alexjg and I discussed this offline and I think we've arrived at a simpler and more flexible solution. It'd be great to have a solution that automerge-repo doesn't even have to know about, so that we can introduce authentication without actually changing the automerge-repo API. Current designIn the design currently proposed by this PR, the repo does three things with the auth provider: 1. Wrap network adapters
const authenticatedAdapters = networkAdapters.map(a => this.auth.wrapNetworkAdapter(a))
this.networkSubsystem = new NetworkSubsystem(authenticatedAdapters, peerId) 2. Provide storage
this.auth.useStorage(this.storageSubsystem) 3. Define sharing policies
const okToAdvertise = await this.repo.auth.okToAdvertise(peerId, documentId)
// etc. Alternative designHere's how these could be achieved without any changes to automerge-repo itself: 1. Wrap network adaptersThe application can wrap the network adapters however it wants before it gives them to the A side benefit to this would be that the application could choose to authenticate some network adapters and not others: For example, a 2. Provide storageThe application can just take the same storage adapter that it gives to the In this case a provider would be using the storage adapter's slightly-lower-level API, and would be responsible for namespacing its keys. 3. Define sharing policiesFor now we can just use the existing So instantiating automerge-repo with auth might look something like this: const storage = new SomeStorageAdapter()
const authProvider = new LocalFirstAuthProvider({
storage, // <- use the same storage adapter
//...
})
const websocketAdapter = new BrowserWebSocketClientAdapter()
const broadcastAdapter = new BroadcastChannelNetworkAdapter()
const network = [
authProvider.wrap(networkAdapter), // <- wrap one but not the other
broadcastAdapter,
]
const sharePolicy = authProvider.getSharePolicy() // <- use the provider's state to make sharing decisions
const repo = new Repo({
network,
storage,
sharePolicy,
}) |
Does the share policy |
@acurrieclark I think we would need to add support for that to |
So effectively if you are not ok to receive from a peer, then you wouldn't be able to sync your changes with that peer anyway? |
Yeah, the only way to really enforce write permissions would be to have the individual automerge changes signed or something. You can't do it on the basis of who you're syncing with, because they might be relaying someone else's changes. |
Closing as this will be superseded by work in progress |
Hey @HerbCaudill, I'm looking for the result of what this was superseded by. Are you able to link to any more recent project? Thanks. |
This is a proposal for an AuthProvider API. This fixes #25 but has evolved a bit from that initial proposal.
Why
Automerge Repo currently offers no way to authenticate a peer, and very little in the way of access control.
Our current security model is the "Rumplestiltskin rule": If you know a document's ID, you can read that document, and everyone else who knows that ID will accept your changes.
That model is good enough for a surprising number of situations — the ID serves as an unguessable secret "password" for the document — but it has limitations. Without a way to establish a peer's identity, we can't revoke access for an individual peer — say if someone leaves a team, or if a device is lost. And we can't distinguish between read and write permissions, or limit access to specific documents.
An application might implement authentication and authorization in any number of ways, so this needs to be pluggable — like the existing network and storage adapters.
Initializing a repo with a specific auth provider might look something like this:
Using the base
AuthProvider
directlyMost of these examples are from the tests.
Adding an
AuthProvider
to a repoUsing the base (maximally permissive) AuthProvider without any overrides is the same as instantiating a repo with no AuthProvider.
Overriding the base adapter's methods
You can instantiate an
AuthProvider
with a config object containing any of the following:okToAdvertise
andokToSync
These are both
SharePolicy
functions. They have the same signature as the existingsharePolicy
config option -- they take a peer ID and optionally a document ID, and return true or false; but they explicitly separate out two questions that the currentsharePolicy
muddles:okToAdvertise
Should we tell this peer about the existence of this document?okToSync
Should we provide this document & changes to it if requested?authenticate
This is a function that takes a
PeerId
and a channel with which to (optionally) communicate with that peer. It returns a promise of anAuthenticationResult
object indicating whether authentication succeeded, and, if not, why.transform
A
Transform
consists of two functions, for transforming inbound and outbound messages, respectively. This might be use, for example, to encrypt messages using a shared secret.Example: Maximally restrictive provider
Example: Restricting access by peer
Example: Restricting access by document
Example: Restricting access by peer and document
Extending
AuthProvider
In the above examples, the provider is customized by passing a config object to the base class's constructor. Alternatively, a custom provider might extend the base class. Here's the maximally restrictive provider we saw earlier:
Example: Using network communication
We'll make a (very insecure) password auth provider that sends a password challenge, and compares the password returned to a hard-coded password list.
Example: Transforming network traffic
The idea here is that rather than authenticate peers, the auth provider encrypts and decrypts messages using a secret key that each peer knows. No keys are revealed, but the peers can only communicate if they know the secret key.