Spaces - interoperable fine-grained permissioning

Hi folks,

Wanted to kick off a discussion around doing fine-grained permissioning on top of spaces. Habitat is developing a working prototype around this, and we’d love to collaborate with other folks to come up with a standard.

Our motivating use case is interoperability of different apps on things like “who is an editor vs commenter vs viewer” in a document app or “who can add/remove members to this channel” in a messaging app.

Our approach is to do a few things: 1) create four opinionated relationships to a space (reader/writer/manager/owner), 2) represent a user-set via the set of DIDs that have one of those relationships on a space, and 3) represent both of those with a lexicon for interoperability. It’s expected that this would be implemented by a space host and is a “peer” to opensocial and simplespace and so compatible with them.

This allows us to do things like create a “group” of people (e.g. Writing Team), and add them all as an editor in our (upcoming) docs app, allow editors to by default be comments, and even add editors of one doc as a commenter of another doc, without needing to maintain separate member lists on multiple spaces that all need to be kept in sync. It also means all the underlying permissioning is readable by any app that gets access to the relevant spaces via the regular space protocol, and the critical parts of the permissioning system are not ‘owned’ by the app (which is one of the hardest parts of doing migrations between different cloud services and applications today–migrating the permissions).

Some of the open questions we have are around space host interoperability: how do two space hosts that both implement this protocol reuse each other’s defined relationships / groups? what is the interoperability story when one space host implements this and one doesn’t–what does an app do? We’re thinking about this in the context of organizations, where both rich permissioning is a necessity for apps as well as cross-organization sharing.

This post dives deeper into our approach: Habitat's road to release: 03 ReBAC on spaces - building [at] habitat ! We’d love to get this group’s thoughts as we start to formalize this beyond a working prototype and into a spec.

10 Likes

A lot of this sounds like what the communities working group is doing, too.

Yes, we want “groups” as a primitive that gets re-used.

1 Like

Yeah open social groups is working on this for larger scale “communities” that will get their own identity. We think there’s room for finer grained permission inheritance and membership without managing multiple identities. Furthermore applications will create more loose “groups” like commenters in a docs app that def shouldn’t require their own identity but should still be interoperable.

1 Like

This is very cool, I’m definitely interested in this as well. I have a new version of Ziran coming that’s built on spaces and I have very similar problems.

Do I understand it correctly that you’re building ReBAC support right into the host PDS? I’ve been considering an approach in which a lexicon would describe updates to a ReBAC schema of some kind, which would then be synced to an existing engine (e.g. SpiceDB) more at an app/arbiter layer. But I’m not sure that’s right.

2 Likes

We are building ReBAC support into the space host.

Because Habitat will be hosting PDS’es for organizations and their members the space host and the host PDS will be the same in this case, but that doesn’t necessarily have to be true. We need to think through all the interoperability flows between space hosts and how it might work.

Also, what we’re doing sounds pretty similar to your approach I think! We are writing the relationships into the space repos themselves, exactly for that reason so the appviews would be able to sync them into a permissions engine. But the space host needs to be aware of the relationship model and the one implementing this API in order to do authz before handing out space credentials.

Cool stuff! This has a lot of similarity to my early idea for the arbiter’s permission design with spaces being able to delegate access to other spaces and with monotonic access levels.

How is this different than the Arbiter?

From Zicklag’s most recent post, the arbiter is more focused on, well, arbiting reads and writes to a shared community or org PDS.

This is not quite true.

With the new design I realized that customizable policies over XRPC calls allows you to implement fine-grained, per-space, record-dependent access control generically, without locking anybody into a specific permission setup. That was the big change from previous designs.[1]

But the full intention is still to allow for fine grained access to spaces. This is exactly what we will need in Roomy, where we have similar document editing, sharing, and commenting needs for our wiki pages.

Putting the arbiter’s policy engine in front of a generic space host would allow it to implement the earlier arbiter’s access level system, simplespace, opensocial, and Habitat’s ReBAC just by providing a different policy.

They can all be expressed as a policy over XRPC calls.

One major open question we have is around space host interoperability: if two space hosts both implement this API, how might they interoperate to support relationships like “readers of space A on space host A now has access to space B on space host B”?

This is something that I thought about quite a lot while designing the access level system for the old arbiter. I have a Quint specification for the old system that helped reveal some gotchas.

You probably won’t run into this particular gotcha, and took a long explanation so here’s it in a “details” block to avoid a huge wall. :laughing:

The biggest gotcha was related to the fact that the ReBAC delegation is recursive and can contain cycles. When implementing this in the old Arbiter I handled it by tracking the set of sub-groups that I had visited to avoid re-visiting so that I didn’t recurse infinitely if there was a cycle. This is apparently what OpenFGA does, too.

This works fine when you are resolving all the groups on the same host. The gotcha had to do when resolving members from remote hosts.

When resolving members from remote hosts I was using the equivalent to your relationship.resolveRelations endpoint, which was expected to return a flattened list of members.

So if host A had a document A/example.doc/1 and it said that everybody with read access to another doc, B/example.doc/2, had read access in A/example.doc/1, then host A would try to call resolveRelations to get the member list of host B’s document.

Because host B is expected to return a flattened member list, it may, in turn, go and call resolveRelations on another host, which might eventually try to resolve the member list of A/example.doc/1 again, creating a loop where the servers all just keep calling each-other’s endpoints in a loop until they run out of memory and crash.

The issue is that there is no recursion state that is passed along when you call another host. It can’t tell if you’re asking for the member list because you’re in a loop, or just because you need it again.

My solution at the time was to pass a “max resolution depth” when making resolveRelations requests. Each host would decrement that when sending triggered resolutions to other servers, and a server that got a request with a resolutionDepth of 0 would return an indication that the user list was possibly incomplete because it needed to resolve another host’s list but it wasn’t allowed to. This would break multi-host cycles without needing a lot of state.[2]

:thinking: Another possible solution would be to resolve the non-flattened member list from remote hosts, and then you can do the recursion and break cycles with a visited list, just like we do locally.

That seems like possibly a better option. I previously liked the simplicity of getting a flattened list from remote endpoints because it allowed the remote service to more easily customize the response without needing to justify why someone was on the list. My impression was that it would make it easier to make external integrations, such as one that returns all the users in a GitHub team as a member of a space.

The point was to not need a lexicon that defines who is a member of the space, so that the actual member list could be computed in a space-host-specific way.

If you commit to using ATProto records for the member lists ( which is a good idea generally, for portability like you point out ), then I think it’s pretty reasonable just to sync those member lists and handle recursion on top of those instead of trying to break recursion with cross-server state.


Anyway, this seems pretty cool!

It doesn’t seem like there are a lot of barriers to turning this in to a spec others could implement.

I think maybe the biggest question is more broadly about how we can, or if it’s possible to, have communities, which can only have one space host, somehow interoperate with groups / roles defined on other kinds of space hosts.

It seems almost unfortunate that a community would necessarily have to pick one kind of space host, and that it excludes it from properly integrating with roles from other communities, but I think that is unavoidable.

I tried to lay a foundation in the arbiter for “policy composition”, such that we can represent a combination of different policy systems for different apps / standards.

I think this might allow one community to simultaneously implement simplespace, opensocial, and habitat relations in the same community, but it’s untested.


I think opensocial sets a good goal of trying to be the simplest thing that we can get a wide audience to agree on. This excludes ReBAC as a goal most-likely.

But I do really like delegating access to other space memberships, which is why the old arbiter did that.

Maybe opensocial could add the ability to make spaces members of roles, as well as DIDs, even though it doesn’t mandate that affecting the actual permissions directly? :thinking:

Maybe that’s just a bad idea. What I’m thinking is that it would be nice if we could have the same role / group membership lexicon or something between opensocial and habitat.

That way hierarchichal roles are portable, even if they are not totally respected by all space hosts.

i.e. that would result in some space hosts not granting all of the access, but it would never result in someone getting more access than they all have.

This idea is half-baked, so not really sure about it at this point. Maybe it’s unnecessary to share a lexicon because the very fact that both standards have public lexicons means that you can migrate from one to the other if you really need to.


  1. This had the bonus effect of being really good for managing shared access to a community PDS account, which was another need we had, so that has been our recent focus. ↩︎

  2. Apparently SpiceDB breaks cycles with a max depth, too. ↩︎

1 Like

To get to maximum awesome, I really want groups / roles to be defined once, and be able to use them across apps.

The way I had envisioned it, apps are going to have to choose to implement support for this.

So I very much see some not necessarily compatible flavours.

With OIDC, one already does a certain level of group mapping, so perhaps there’s a way forward there.

2 Likes

Yeah we’ve so far been focused on enabling apps with a more powerful permissions model than simplespace that is legible to users and apps. We’re trying to discover which low level primitives are best from app developers to build upon (self promo chalk). We haven’t fully fleshed out how it would work across space hosts.

However, since the ACL is encoded as records, if a space host A wants to inherit permissions from a space on space host B, it can crawl that space to get the ACL and index it locally. Haven’t thought through all the details of that but I think it generally should address the issue of recursion?

1 Like

Yeah, I think so. I realized that at the end of writing my “details” section above, that syncing all the roles and member records before trying to compute the flat list locally means you can break recursion locally instead of cross-host, which fixes the issue.

1 Like

I also don’t think it’s true that a community has to pick one kind of space host. We may want high-level “communities” to be defined once and reused across apps, but this seems totally compatible with other protocols for permissioning smaller artifacts, e.g. simplespace or what we’re laying out here.

The protocol also allows apps to be space hosts, so I think it leaves the door open to many space hosts with different APIs, of course some being incompatible with others. That’s why we’re trying to think about what one cross-app compatible way of doing rich permissioning might be that can be implemented by space hosts.

Edit: I don’t have a great name for this. “groups” is too easily confused with the open social work, “rich permissioning” / “fine grained permissioning” feels vague.

2 Likes