Discord authz model
# spicedb
t
Has anyone written a schema to model Discord's authorization? Specifically, I'm curious about these attributes: - Server roles with Allow/Deny on each permission, users assigned these roles - Channels that inherit permissions from Server, but also allow setting permissions for specific roles or for specific users with Allow/Deny/Inherit on each permission - According to Discord's developer docs, this is done with Permission Overwrites (https://discord.com/developers/docs/topics/permissions#permission-overwrites), which map a user/role to a channel by associating
allow
and
deny
fields for their permissions. It would be neat to see this represented in the schema, but not necessary - Bonus: Threads inside channels, but can additionally be public (to the channel) or private to specific members. - I imagine public/private would be represented with a caveat?
v
Not to my knowledge! Have you started working on this? Would you like to give it a shot?
Essentially this is: - custom roles - hierarchical overrides I modeled something similar in a past job with SpiceDB successfully. Public / private is modeled using wildcards
t
I'm happy to give it a go! Seems like a fun learn-by-doing task, I'll send my attempt in this thread
Here are two schemas: one with custom roles and cascading permissions, and another with the addition of hierarchical overrides. A few notes: - Discord has 50 different permissions – for the sake of learning, I vastly simplified this to just read and write (messages) permissions. - Discord treats "threads" as temporary channels whose parent is another channel. I decided the
parent
relation on a
channel
could capture this well enough, and left out explicit mention of threads. Application-level logic can handle preventing infinite nesting. - In the context of Discord, public/private is the same as checking if the built-in role
@ everyone
has read access, which is adequately handled by standard role logic. - An improvement to this schema would include
built_in_role
as an additional relation, only including
@ everyone
. Left out for now. - Overrides! These are objects where the subject either is or isn't included on each individual relation. A resource then relates to an override via an
allow
or
deny
relation. If neither exist, then the resource inherits permissions from the parent. - https://discord.com/developers/docs/resources/channel#overwrite-object - This is the overwrite "object" that Discord exposes, implying that they store it as its own DB table. I modeled the schema to sorta match - For some reason, Discord allows by default? As in, if someone has a role that explicitly denies a permission and also a role that explicitly grants the permission, Discord will grant the permission. This isn't the choice I would've made, but to stay true to reality, I modeled the schema this way https://cdn.discordapp.com/attachments/1248079453041459270/1248391718014025870/discord0.zed?ex=66637ee3&is=66622d63&hm=04734caf6029ee9cdc4e62d28e86517a42398b687d23e6d8a86f41de450ea330& https://cdn.discordapp.com/attachments/1248079453041459270/1248391718353506386/discord1.zed?ex=66637ee3&is=66622d63&hm=ffd110915769c94fe21af0ce8915bbecbc8d63b3ebe7d5761130f07a03cf82e3&
How does that look to you? Any improvements you would suggest, or obvious mistakes that you see?
(I would consider an unintelligible schema to be a mistake, so let me know if it's not making sense)
Looking at it now, I'm not sure I got category/channel permissions right, specifically the parentheses. This would successfully allow overwriting the way the comment requires, but it seems like it would poorly parallelize.
permission read = ((((parent->read) - deny_role->read) + allow_role->read) - deny_user->read) + allow_user->read
(The comment) Source: https://discord.com/developers/docs/topics/permissions#permission-overwrites Process for determining access: - Add access granted by the parent - Remove access denied to the role by an override - Add access granted to the role by an override - Remove access denied to the user by an override - Add access granted to the user by an override
v
sorry if I don't get back to you, reviewing these things are time consuming and we can only do that in a best effort basis. I'd perhaps suggest getting these examples in a playground schema link so that we all can review it there. It would also be convenient to have assertions to make sure the behaviour is what you actually intend. You can click the "share" button to share it with us.
t
No worries, I understand – and I can definitely try putting them in playgrounds 👍
v
I have to give it more thought but took a stab: https://play.authzed.com/s/ef7QmjT8yDCj/schema
In your first attempt, the concept of
role
didn't seem to aggregate any permissions, just users, and all grants are handled via an "overwrite", which seems required even when there are no overwrites. My approach uses the "custom roles" pattern where roles and assignment are decoupled.
t
Oh awesome! Thanks for doing that, I like that the whole overwrite hierarchy is included
There's something I don't quite understand though, could you clarify the difference between this (what you have):
Copy code
definition role {
  relation allow_send_messages: user:*
  relation admin: user:*
}

definition role_grant {
  relation assignees: user | guild#member
  relation role: role

  permission send_messages = assignees & role->allow_send_messages
  permission admin = assignees & role->admin 
}
and this (no
role_grant
)?
Copy code
definition role {
  relation assignees: user | guild#member
  relation allow_send_messages: user:*
  relation allow_admin: user:*

  permission send_messages = assignees & allow_send_messages
  permission admin = assignees & allow_admin 
}
I guess I can imagine decoupling roles and assignments is useful, and now that I'm playing with it, it does feel better. It's just different from the pattern in this blog post (https://authzed.com/blog/user-defined-roles) and I'm less familiar with it, so I thought I'd dig into it
I think this is also important to check, to ensure priority of permissions: is this
permission send_messages = parent->send_messages - everyone_deny_overwrite->send_messages + everyone_allow_overwrite->send_messages - role_deny_overwrite->send_messages + role_allow_overwrite->send_messages
equivalent to this
permission send_messages = ((((parent->send_messages - everyone_deny_overwrite->send_messages) + everyone_allow_overwrite->send_messages) - role_deny_overwrite->send_messages) + role_allow_overwrite->send_messages)
and not this?
permission send_messages = (parent->send_messages - everyone_deny_overwrite->send_messages) + (everyone_allow_overwrite->send_messages - role_deny_overwrite->send_messages) + role_allow_overwrite->send_messages
Cleaner:
a - b + c - d + e
vs
((((a - b) + c) - d) + e)
vs
(a - b) + (c - d) + e
Update: I did have to put these parentheses to get desired behavior:
((((a - b) + c) - d) + e)
https://play.authzed.com/s/UtB-K293j2Sh/relationships Made some test relationships and assertions
https://play.authzed.com/s/czBz21SN_ens/schema Another very similar schema with some minor tweaks, converted test relationships: - Consolidated override mechanism, so all channel overrides use the
override
type -
role
has a direct mapping to the
Role
that end-users interact with - Or more explicitly, the mapping
{ Discord Roles } -> { SpiceDB roles }
is surjective - In the previous schema, I had to make "phantom roles" (o1, o2, and o3) to do the overrides -
permission_grant
is both how
roles
get their permissions and how
overrides
get their
extra_permissions
- This type is spicedb-specific, it doesn't get its own table in the standard DB
s
@theconductor - This thread was the inspiration for an upcoming livestream!

https://www.youtube.com/watch?v=qmyH9CXzQhM

t
Oh wow, what a blast! I'm sorry I missed this, I haven't checked Discord super often, but thanks for rolling that up into one awesome presentation!
8 Views