js-ipld icon indicating copy to clipboard operation
js-ipld copied to clipboard

Pass down the `options` object to resolver.resolve()

Open xavivives opened this issue 8 years ago • 12 comments

I have a use case were I want to have some sort of authentication when resolving through each node. This requires the local resolver to have access to the authentication data.

It seems that the options object could transport any arbitrary data and could fit my purpose as well as other ones. Right now the options object is not passed down to resolver.resolve(binaryBlob, path, callback

I'm opening this issue as it was suggested by @vmx here

xavivives avatar Apr 12 '18 16:04 xavivives

This probably shouldn't be done on a format-by-format basis. Instead, I'd write some form of wrapping IPLDResolver. That is:

let resolver = new RestrictedIPLDResolver(resolver, authService, peer)
resolver.get(...)

The wrapping resolver would check if a peer (by peer ID) can access any given node. It can do this by:

  1. Looking inside the node.
  2. Checking the CID against some ACL.
  3. Running some interactive authentication protocol with the remote peer.

etc...

Formats really just translate between bytes and abstract IPLD objects.

Stebalien avatar Apr 17 '18 02:04 Stebalien

@Stebalien I think that could a solution for this case. But I think passing on things is also valuable to make things, such as authorization possible. If it turns out to be useful, it can then be done e.g. with a wrapper as mentioned above.

For the wrapper the formats would need some hooks (or whatever you might call it) to be able to do custom things while traversing.

vmx avatar Apr 18 '18 08:04 vmx

@Stebalien If I understand correctly your solution requires to call resolver.get() node by node , which seems to defeat the purpose of using the resolver.

xavivives avatar Apr 18 '18 11:04 xavivives

@xavivives It only requires one call to resolver.get(...) which will make multiple format.resolve(...) invocations. That is, it performs the access check for each node it encounters on the path.

@vmx IPLD nodes need to be completely self describing. Given an IPLD node, a resolver should be able to decode it without any additional context. Can you think of an example of a case where the format-specific resolver would need to perform authentication? That is, a case where this authentication couldn't be performed by the outer IPLDResolver service?

Stebalien avatar Apr 18 '18 14:04 Stebalien

@Stebalien You are absolutely right (sorry, I confused myself a bit). A wrapping resolver should solve all use cases I had in mind. Though in this case it might start as format specific, e.g. where within the node to find the authorization information, but that could later on be parameterized.

@xavivives Your use case sounds great to create such a wrapping resolver which could serve as an example.

vmx avatar Apr 18 '18 14:04 vmx

Though in this case it might start as format specific, e.g. where within the node to find the authorization information, but that could later on be parameterized.

Ideally, it would just look at the same place (path), regardless of format. However, we don't work in an ideal world.

Stebalien avatar Apr 19 '18 01:04 Stebalien

I get your point and it makes sense to me.

I was trying to avoid any unnecessary resolution, if the request is not allowed to access the first node, why resolve it all?

At the point I'am this is a completely unnecessary optimization, but if can play the devil's advocate... Imagine this usecase:

Each User (node) has its own file-system tree. The leaves are the data. Which can be just a CID but also a Merkle Path poiting to content of other users.

User1 > folder1 > secretFolder

User2 > folder2 > [User1 > folder1 > secretFolder]

User3 > folder3 > [User2 > folder2 > [User1 > folder1 > secretFolder]]

If the User3 wants to resolve the path to access secretFolder it means that User2 will have to resolve the request on her local machine, which requires to ask User1 to resolve it as well.

Let's say that User3 does not have access to User2 > folder2. If we just wrap authenticate() around resolve(), User2 will still make a request to User1, which will still resolve() on its own machine.

There are probably dozens of workarounds to this, but it seems something works considering.

xavivives avatar Apr 21 '18 09:04 xavivives

@xavivives I don't understand your example. If User3 wants to access secretFolder, why would User2 resolve something if User3 doesn't have permission to User2's data?

vmx avatar Apr 23 '18 10:04 vmx

@vmx if I understand correctly @Stebalien sugestion is to do this: authenticate(resolve(secretFolderPah)) as oppose my original intend: resolve(authenticate(secretFolderPath)).

In my use case the access is granted by folder. Meaning that each folder (an IPLD object) has a list of public keys that are verified prior returning the resolved content.

If I want to resolve the content by doing authenticate(resolve(secretFolderPah)), all the involved nodes in the path will have to call resolve(), despite the accessibility credentials. And of course, they will return the accessible content only.

While if we do resolve(authenticate(secretFolderPath)) the resolution chain will stop as soon as the it detects that the user has no rights.

xavivives avatar Apr 24 '18 17:04 xavivives

Using the previous example. The call stack would end up being something like this: If we use authenticate(resolve(secretFolderPah)) :

User3.resolve()
User2.resolve()
User1.resolve()
User1.authenticate()
User2.authenticate() ---> Detects it does not have access

While if we use do resolve(authenticate(secretFolderPath))

User3.authenticate()
User3.resolve()
User2.authenticate() -->Detects it does not have access

xavivives avatar Apr 24 '18 17:04 xavivives

Actually, looking at how IPLDResolver is implemented, you may need to implement a new IPLDResolver instead of wrapping it (unless we make it pluggable).

I was relying on two facts:

  1. Resolve is iterative (object by object). Unfortunately, the IPLDResolver doesn't expose this.
  2. Permissions can only reasonably be enforced at the object boundaries.

So, the custom ipldResolver.resolve(root, path) would be defined as:

// synchronous/error-less to make this simpler
function resolve(rootCid, path) { // <-- IPDLResolver
    let cid = rootCid
    let remainder = path
    while (remainder.length > 0) {
        // This is where permissions will be enforced.
        // This function may do some internal resolution to do object-level permission checks.
        let node = getAuthenticated(cid) // Could also pass, e.g., the rootCid, path, pid, etc.
        let {cid, remainder} = util.resolve(node, remainder) // <-- the function in question
    }
    return getAuthenticated(cid)
}

Basically, there's no reason to introduce any format-specific logic.


@vmx this does highlight an inefficiency in the js-ipld interfaces. Not allowing IPLD objects to cache partially (or fully) deserialized objects will force resolvers to deserialize the object multiple times in cases like this.


Introducing ACL hooks into the IPLDResolver would help a lot in this case.

  • get-node (cid -> node)
  • begin-resolve ((node, path) -> node)
  • resolve (node, path) -> (nextNode, remainder) // possibly with extra root CID, etc. context.
  • etc...

Stebalien avatar Apr 24 '18 18:04 Stebalien

Would https://github.com/ipld/js-ipld/pull/122 help? You could get a pull stream with all the nodes on they way you are resolving.

@Stebalien I agree. This is a good issue to show all the short-comings of the current implementation. I would concentrate on making things work first and looking into inefficiencies like multiple useless deserializations.

vmx avatar Apr 25 '18 10:04 vmx