Skip to content
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

Discord permissions system example #145

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 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
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@ cedar-rust-hello-world/target/*

# Don't check build files for tinytodo
tinytodo/Cargo.lock
tinytodo/target/*
tinytodo/target/*

# Don't check build files for cedar-discord
cedar-example-use-cases/discord/Cargo.lock
cedar-example-use-cases/discord/target/*
6 changes: 6 additions & 0 deletions cedar-example-use-cases/discord/ALLOW/oflatt_manage_role.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"principal": "User::\"oflatt\"",
"action": "Action::\"ManageRole\"",
"resource": "Role::\"everyone\"",
"context": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"principal": "User::\"yihong\"",
"action": "Action::\"SendMessage\"",
"resource": "Server::\"test\"",
"context": {}
}
6 changes: 6 additions & 0 deletions cedar-example-use-cases/discord/DENY/yihong_kick_oliver.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"principal": "User::\"yihong\"",
"action": "Action::\"KickMember\"",
"resource": "Server::\"test\"",
"context": {}
}
6 changes: 6 additions & 0 deletions cedar-example-use-cases/discord/DENY/yihong_manage_role.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"principal": "User::\"yihong\"",
"action": "Action::\"ManageRole\"",
"resource": "Role::\"everyone\"",
"context": {}
}
27 changes: 27 additions & 0 deletions cedar-example-use-cases/discord/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Cedar Discord Example

This repository contains a limited model of the [discord permissions system](https://support.discord.com/hc/en-us/articles/206029707-Setting-Up-Permissions-FAQ).


The file `src/main.rs` sets up example users and demonstrates they have different permissions based on the different roles.
Discord is interesting because users may have multiple roles and some users may also set the permissions of other roles dynamically.
In this example, we implement this functionality by using
Cedar's parent system to build a DAG that looks something like this:

```
Permission::"SendMessage" Permission::"KickMember"
▲ ▲ ▲
│ └───────────────────┐ │
│ │ │
Role::"everyone" Role::"admin"
▲ ▲
│ │
User::"yihong" User::"oflatt"
Copy link
Contributor

Choose a reason for hiding this comment

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

IIUC Discord correctly, a role is assigned permissions for a particular channel. So I don't see how it makes sense for the hierarchy to just relate Role to Permission.

I would think you need to set it up so that you have Channel resource, and that when the operator assigns permissions to a channel for a role, you basically create an ad hoc policy that expresses those permissions.

You also seem to be missing the concepts of Category for channels (which can be "synced" or not), and the fact that permissions can apply to all channels (server wide). I would think you need a Server object which channels are in.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great ideas!
I suggest that we merge this PR without channels (I'll re-name Channel to Server for now)
I'll submit a follow-up PR that introduces channels, and another for categories.

```


We can then user Cedar's `in` construct to check if the permission
is reachable from a given user.
Note that it's currently unclear if this is the best way to use
Cedar for discord's permissions model. Another approach is to generate
many Cedar policies, one per role and permission pair.
58 changes: 58 additions & 0 deletions cedar-example-use-cases/discord/entities.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
[
{
"uid": {
"type": "Server",
"id": "general"
},
"attrs": {},
"parents": []
},
{
"uid": {
"type": "Permission",
"id": "SendMessage"
},
"attrs": {},
"parents": []
},
{
"uid": {
"type": "Role",
"id": "everyone"
},
"attrs": {},
"parents": [
{
"type": "Permission",
"id": "SendMessage"
}
]
},

{
"uid": {
"type": "User",
"id": "yihong"
},
"attrs": {},
"parents": [
{
"type": "Role",
"id": "everyone"
}
]
},
{
"uid": {
"type": "User",
"id": "oflatt"
},
"attrs": {},
"parents": [
{
"type": "Role",
"id": "owner"
}
]
}
]
18 changes: 18 additions & 0 deletions cedar-example-use-cases/discord/policies.cedar
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Allow SendMessage when the user has the SendMessage permission
permit (
oflatt marked this conversation as resolved.
Show resolved Hide resolved
principal in Permission::"SendMessage",
action == Action::"SendMessage",
resource == Server::"test"
);

permit (
principal in Permission::"KickMember",
action == Action::"KickMember",
resource == Server::"test"
);

permit (
principal in Role::"owner",
action == Action::"ManageRole",
resource == Role::"everyone"
);
28 changes: 28 additions & 0 deletions cedar-example-use-cases/discord/policies.cedarschema
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// discord permissions, (e.g. SendMessage)
entity Permission;

// discord roles, (e.g. the default "everyone" role)
// discord roles have user-configurable permissions
entity Role in [Permission];

// discord users, the entity id being the user id
// discord users may have multiple roles
entity User in [Role];

// TODO: add Channels
// permissions in discord are specific to channels

// a discord server
// currently, we only consider a single server "test"
entity Server;


action SendMessage, KickMember appliesTo {
principal: [User],
resource: [Server]
};

action ManageRole appliesTo {
principal: [User],
resource: [Role]
};
6 changes: 6 additions & 0 deletions cedar-example-use-cases/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,10 @@ validate "tags_n_roles" "policies.cedar" "policies.cedarschema"
authorize "tags_n_roles" "policies.cedar" "entities.json" "policies.cedarschema"
format "tags_n_roles" "policies.cedar"

# Discord
echo -e "\nTesting Discord..."
validate "discord" "policies.cedar" "policies.cedarschema"
authorize "discord" "policies.cedar" "entities.json" "policies.cedarschema"
format "discord" "policies.cedar"

exit "$any_failed"
6 changes: 5 additions & 1 deletion cedar-rust-hello-world/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ publish = false
serde_json = "1.0"

[dependencies.cedar-policy]
version = "3.0.0"
version = "4.0.0"
git = "https://github.com/cedar-policy/cedar"
branch = "main"
#Do not add any lines below this. CI relies on the previous line being the second-to-last line in the file

64 changes: 17 additions & 47 deletions cedar-rust-hello-world/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ fn print_response(ans: Response) {
/// This uses the waterford API to call the authorization engine.
fn execute_query(request: &Request, policies: &PolicySet, entities: Entities) -> Response {
let authorizer = Authorizer::new();
authorizer.is_authorized(request, &policies, &entities)
authorizer.is_authorized(request, policies, &entities)
}

fn validate() {
Expand All @@ -374,52 +374,22 @@ fn validate() {

};
"#;
let sc = r#"
{
"": {
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"age": {
"type": "Long"
}
}
},
"memberOfTypes": [
"UserGroup"
]
},

"UserGroup": {
"memberOfTypes": []
},

"Album": {
"memberOfTypes": [
"Album"
]
}
},
"actions": {
"view": {
"appliesTo": {
"resourceTypes": [
"Album"
],
"principalTypes": [
"User"
]
}
}
}
}
}
"#;

let p = PolicySet::from_str(src).unwrap();
let schema = Schema::from_str(sc).unwrap();

let schema_text = r#"
entity UserGroup;
entity User in [UserGroup] = {
"age": Long
};
entity Album;
action view appliesTo {
principal: [User],
resource: [Album]
};
"#;
// the schema can be parsed in rust:
let (schema, warnings) = Schema::from_str_natural(schema_text).unwrap();
assert_eq!(warnings.count(), 0);
let validator = Validator::new(schema);

let result = Validator::validate(&validator, &p, ValidationMode::default());
Expand Down Expand Up @@ -453,7 +423,7 @@ fn annotate() {
let ans = execute_query(&request, &policies, Entities::empty());
for reason in ans.diagnostics().reason() {
//print all the annotations
for (key, value) in policies.policy(&reason).unwrap().annotations() {
for (key, value) in policies.policy(reason).unwrap().annotations() {
println!("PolicyID: {}\tKey:{} \tValue:{}", reason, key, value);
}
}
Expand Down
Loading