diff --git a/tinytodo/Cargo.toml b/tinytodo/Cargo.toml
index a06851c..dc55b4e 100644
--- a/tinytodo/Cargo.toml
+++ b/tinytodo/Cargo.toml
@@ -21,6 +21,7 @@ notify = { version = "5.1.0", default-features = false, features = ["macos_kqueu
use-templates = []
[dependencies.cedar-policy]
+features = ["partial-eval"]
version = "4.0.0"
git = "https://github.com/cedar-policy/cedar"
branch = "main"
diff --git a/tinytodo/entities.json b/tinytodo/entities.json
index f20de8f..0f3cca0 100644
--- a/tinytodo/entities.json
+++ b/tinytodo/entities.json
@@ -41,18 +41,27 @@
"teams": {
"Team::\"temp\"": {
"uid": "Team::\"temp\"",
+ "owner": "User::\"emina\"",
+ "admins": ["User::\"emina\""],
+ "name": "temp",
"parents": [
"Application::\"TinyTodo\""
]
},
"Team::\"admin\"": {
"uid": "Team::\"admin\"",
+ "owner": "User::\"emina\"",
+ "admins": ["User::\"emina\""],
+ "name": "admin",
"parents": [
"Application::\"TinyTodo\""
]
},
"Team::\"interns\"": {
"uid": "Team::\"interns\"",
+ "owner": "User::\"emina\"",
+ "admins": ["User::\"emina\""],
+ "name": "interns",
"parents": [
"Application::\"TinyTodo\"",
"Team::\"temp\""
diff --git a/tinytodo/frontend/static/hello.css b/tinytodo/frontend/static/hello.css
new file mode 100644
index 0000000..789e04e
--- /dev/null
+++ b/tinytodo/frontend/static/hello.css
@@ -0,0 +1,28 @@
+
+ul.navbar {
+ list-style: none
+}
+
+ul.navbar li {
+ display: inline-block;
+ padding: 0 7px;
+ position: relative;
+}
+
+ul.navbar li:not(:last-child)::after {
+ content: "";
+ border: 1px solid #e2e2e2;
+ border-width: 1px 1px 0 0 ;
+ position: absolute;
+ right: -3px;
+ top: 0;
+ height: 100%;
+}
+
+.inlineSubmit {
+ background: none;
+ border: none;
+ color: blue;
+ text-decoration: underline;
+ cursor: pointer;
+}
diff --git a/tinytodo/frontend/templates/index.html b/tinytodo/frontend/templates/index.html
new file mode 100644
index 0000000..85eb4d6
--- /dev/null
+++ b/tinytodo/frontend/templates/index.html
@@ -0,0 +1,5 @@
+{% extends "main.html" %}
+ {% block title %}Home{% endblock %}
+ {% block body %}
+ Tiny Todo Home
+ {% endblock %}
\ No newline at end of file
diff --git a/tinytodo/frontend/templates/list/create.html b/tinytodo/frontend/templates/list/create.html
new file mode 100644
index 0000000..053f9e4
--- /dev/null
+++ b/tinytodo/frontend/templates/list/create.html
@@ -0,0 +1,10 @@
+{% extends "main.html" %}
+{% block title %}Create List{% endblock %}
+{% block body %}
+
Create A List
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/tinytodo/frontend/templates/list/index.html b/tinytodo/frontend/templates/list/index.html
new file mode 100644
index 0000000..4188d10
--- /dev/null
+++ b/tinytodo/frontend/templates/list/index.html
@@ -0,0 +1,101 @@
+{% extends "main.html" %}
+{% block title %} List Viewer {% endblock %}
+{% block body %}
+
+{{name}}
+Contents
+
+ {% for item in items %}
+ {{item.name}}
+
+
+ {% endfor %}
+
+ Add item:
+
+Sharing
+Add reader:
+
+Add editor:
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/tinytodo/frontend/templates/lists.html b/tinytodo/frontend/templates/lists.html
new file mode 100644
index 0000000..78939ad
--- /dev/null
+++ b/tinytodo/frontend/templates/lists.html
@@ -0,0 +1,15 @@
+{% extends "main.html" %}
+{% block title %}
+Lists
+{% endblock %}
+{% block body %}
+Create a new list
+My Lists
+
+{% endblock %}
\ No newline at end of file
diff --git a/tinytodo/frontend/templates/login.html b/tinytodo/frontend/templates/login.html
new file mode 100644
index 0000000..267b938
--- /dev/null
+++ b/tinytodo/frontend/templates/login.html
@@ -0,0 +1,8 @@
+{% extends "main.html" %}
+{% block title %} Login {% endblock %}
+{% block body %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/tinytodo/frontend/templates/main.html b/tinytodo/frontend/templates/main.html
new file mode 100644
index 0000000..730c197
--- /dev/null
+++ b/tinytodo/frontend/templates/main.html
@@ -0,0 +1,30 @@
+
+
+
+
+ >TinyTodo - {% block title %}{% endblock %}
+
+
+
+
+ {% block body %} {% endblock %}
+
+
\ No newline at end of file
diff --git a/tinytodo/frontend/templates/team/create.html b/tinytodo/frontend/templates/team/create.html
new file mode 100644
index 0000000..73a0283
--- /dev/null
+++ b/tinytodo/frontend/templates/team/create.html
@@ -0,0 +1,10 @@
+{% extends "main.html" %}
+{% block title %}Create List{% endblock %}
+{% block body %}
+ Create A Team
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/tinytodo/frontend/templates/team/index.html b/tinytodo/frontend/templates/team/index.html
new file mode 100644
index 0000000..48f00aa
--- /dev/null
+++ b/tinytodo/frontend/templates/team/index.html
@@ -0,0 +1,38 @@
+{% extends "main.html" %}
+{% block title %} List Viewer {% endblock %}
+{% block body %}
+
+{{name}}
+
+Admins
+
+ Add Admin:
+
+
+ Remove Admin:
+
+
+Members
+
+ Add Member:
+
+
+ Remove Member:
+
+{% endblock %}
\ No newline at end of file
diff --git a/tinytodo/frontend/templates/teams.html b/tinytodo/frontend/templates/teams.html
new file mode 100644
index 0000000..f2288f1
--- /dev/null
+++ b/tinytodo/frontend/templates/teams.html
@@ -0,0 +1,24 @@
+{% extends "main.html" %}
+
+{% block title %} My Teams {% endblock %}
+{% block body %}
+ My Teams
+Create a new team
+Teams you can add/remove admins
+
+ {% for team in admin_teams %}
+
+ {{team.name}}
+
+ {% endfor %}
+
+Teams you can add/remove members
+
+ {% for team in member_teams %}
+
+ {{team.name}}
+
+ {% endfor %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/tinytodo/frontend/tinytodo-web.py b/tinytodo/frontend/tinytodo-web.py
new file mode 100644
index 0000000..0a2d6f9
--- /dev/null
+++ b/tinytodo/frontend/tinytodo-web.py
@@ -0,0 +1,218 @@
+import flask
+import requests
+import secrets
+
+APP = flask.Flask(__name__)
+# Lol don't do this
+APP.secret_key = secrets.token_bytes(16)
+
+def render_page(page, **kwargs):
+ if 'username' in flask.session:
+ print('is logged in')
+ return flask.render_template(page, user=flask.session['username'], **kwargs)
+ else:
+ return flask.render_template(page, **kwargs)
+
+@APP.route("/")
+def index():
+ return render_page('index.html')
+
+@APP.route('/login', methods=['GET', 'POST'])
+def login():
+ if flask.request.method == 'POST':
+ username = flask.request.form['username']
+ flask.session['username'] = username
+ return flask.redirect(flask.url_for('index'))
+ else:
+ return render_page('login.html')
+
+@APP.route("/logout")
+def logout():
+ flask.session.pop('username', None)
+ return flask.redirect(flask.url_for('index'))
+
+@APP.route('/lists/')
+def get_lists():
+ r = requests.get('http://localhost:8080/api/lists/get', params= {'uid': 'User::"{0}"'.format(flask.session['username'])})
+ return render_page('lists.html', lists=r.json())
+
+@APP.route('/teams')
+def get_teams():
+ r = requests.get('http://localhost:8080/api/team/manage/member/get', params= {'uid': 'User::"{0}"'.format(flask.session['username'])})
+ member_teams = list(filter(lambda x: x['name'] is not None, r.json()))
+ r = requests.get('http://localhost:8080/api/team/manage/admin/get', params= {'uid': 'User::"{0}"'.format(flask.session['username'])})
+ admin_teams = list(filter(lambda x: x['name'] is not None, r.json()))
+ return render_page('teams.html', member_teams=member_teams, admin_teams=admin_teams)
+
+@APP.route('/list/create', methods=['GET'])
+def get_create_list():
+ return render_page('list/create.html')
+
+@APP.route('/list/create', methods=['POST'])
+def post_create_list():
+ try:
+ name = flask.request.form['name']
+ owner = 'User::"{0}"'.format(flask.session['username'])
+ data = {'uid': owner, 'name': name}
+ requests.post('http://localhost:8080/api/list/create', json=data)
+ return flask.redirect(f'/lists')
+ except KeyError:
+ return 'Bad args!', 403
+
+@APP.route('/list/read')
+def get_list():
+ list = flask.request.args['name']
+ r = requests.get('http://localhost:8080/api/list/get', params= {'uid': 'User::"{0}"'.format(flask.session['username']), 'list': list})
+ l = r.json()
+ return render_page('list/index.html', id=l['uid'], name=l['name'], items=l['tasks'], readers=l['readers'], editors=l['editors'])
+
+@APP.route('/list/add_item', methods=['POST'])
+def add_item():
+ user = 'User::"{0}"'.format(flask.session['username'])
+ list_uid = flask.request.form['name']
+ task_name = flask.request.form['item']
+ data = {
+ 'uid': user,
+ 'list': list_uid,
+ 'name': task_name,
+ }
+ r = requests.post('http://localhost:8080/api/task/create', json=data)
+ if r.status_code == 200:
+ body = r.json()
+ if is_error(body):
+ if is_authz_denied(body):
+ return flask.redirect(f'/auth_denied')
+ else:
+ print('Error: %s' % body['error'])
+ return flask.redirect(f'/lists')
+
+@APP.route('/list/delete_task', methods=['POST'])
+def delete_item():
+ user = 'User::"{0}"'.format(flask.session['username'])
+ list_uid = flask.request.form['name']
+ task_name = flask.request.form['task_id']
+ data = {
+ 'uid': user,
+ 'list': list_uid,
+ 'task': task_name,
+ }
+ r = requests.delete('http://localhost:8080/api/task/delete', json=data)
+ print(r)
+ print(data)
+ if r.status_code == 200:
+ body = r.json()
+ if is_error(body):
+ if is_authz_denied(body):
+ return flask.redirect(f'/auth_denied')
+ else:
+ print('Error: %s' % body['error'])
+ return flask.redirect(f'/lists')
+
+@APP.route('/list/share_with', methods = ['POST'])
+def share_with():
+ user= 'User::"{0}"'.format(flask.request.form['user'])
+ data = {
+ 'uid': 'User::"{0}"'.format(flask.session['username']),
+ 'list': flask.request.form['list_id'],
+ 'share_with': user,
+ 'role': flask.request.form['share_kind']
+ }
+ r = requests.post('http://localhost:8080/api/share', json=data)
+ if r.status_code == 200:
+ body = r.json()
+ if is_error(body):
+ if is_authz_denied(body):
+ return flask.redirect(f'/auth_denied')
+ else:
+ print('Error: %s' % body['error'])
+ return flask.redirect(f'/lists')
+
+@APP.route('/team/create', methods=['GET'])
+def get_create_team():
+ return render_page('team/create.html')
+
+@APP.route('/team/create', methods=['POST'])
+def post_create_team():
+ try:
+ id = flask.request.form['name']
+ owner = flask.session['username']
+ data = {'owner': owner, 'id': id}
+ requests.post('http://localhost:8080/api/team/create', json=data)
+ return flask.redirect(f'/teams')
+ except KeyError:
+ return 'Bad args!', 403
+
+@APP.route('/auth_denied')
+def auth_denied():
+ return 'You are not authorized to perform this action', 403
+
+@APP.route('/team/read')
+def get_team():
+ uid = flask.request.args['uid']
+ r = requests.get('http://localhost:8080/api/team/get', params= {'uid': uid})
+ l = r.json()
+ return render_page('team/index.html', id=l['uid'], name=l['name'],)
+
+@APP.route('/team/remove_admin', methods=['POST'])
+def remove_admin():
+ data = {
+ 'team': flask.request.form['name'],
+ 'user': flask.session['username'],
+ 'candidate': flask.request.form['candidate']
+ }
+ r = requests.delete('http://localhost:8080/api/team/admin/remove', json=data)
+ if r.status_code == 200:
+ body = r.json()
+ if is_error(body):
+ if is_authz_denied(body):
+ return flask.redirect(f'/auth_denied')
+ else:
+ print('Error: %s' % body['error'])
+ return flask.redirect(f'/teams')
+
+@APP.route('/team/add_admin', methods=['POST'])
+def add_admin():
+ data = {
+ 'team': flask.request.form['name'],
+ 'user': flask.session['username'],
+ 'candidate': flask.request.form['candidate']
+ }
+ requests.post('http://localhost:8080/api/team/admin/add', json=data)
+ return flask.redirect(f'/teams')
+
+@APP.route('/team/remove_member', methods=['POST'])
+def remove_member():
+ data = {
+ 'team': flask.request.form['name'],
+ 'user': flask.session['username'],
+ 'candidate': flask.request.form['candidate']
+ }
+ r = requests.delete('http://localhost:8080/api/team/member/remove', json=data)
+ if r.status_code == 200:
+ body = r.json()
+ if is_error(body):
+ if is_authz_denied(body):
+ return flask.redirect(f'/auth_denied')
+ else:
+ print('Error: %s' % body['error'])
+ return flask.redirect(f'/teams')
+
+@APP.route('/team/add_member', methods=['POST'])
+def add_member():
+ data = {
+ 'team': flask.request.form['name'],
+ 'user': flask.session['username'],
+ 'candidate': flask.request.form['candidate']
+ }
+ requests.post('http://localhost:8080/api/team/member/add', json=data)
+ return flask.redirect(f'/teams')
+
+def is_error(body):
+ return type(body) is dict and 'error' in body
+
+def is_authz_denied(body):
+ return 'error' in body and body['error'] == 'Authorization Denied'
+
+if __name__ == '__main__':
+ APP.debug = True
+ APP.run()
diff --git a/tinytodo/policies.cedar b/tinytodo/policies.cedar
index b33e1f6..697d1bd 100644
--- a/tinytodo/policies.cedar
+++ b/tinytodo/policies.cedar
@@ -1,16 +1,12 @@
// Policy 0: Any User can create a list and see what lists they own
permit (
principal,
- action in [Action::"CreateList", Action::"GetLists"],
+ action in [Action::"CreateList", Action::"GetLists", Action::"CreateTeam"],
resource == Application::"TinyTodo"
);
// Policy 1: A User can perform any action on a List they own
-permit (
- principal,
- action,
- resource is List
-)
+permit (principal, action, resource is List)
when { resource.owner == principal };
// Policy 2: A User can see a List if they are either a reader or editor
@@ -40,7 +36,7 @@ when { principal in resource.editors };
// action,
// resource in Application::"TinyTodo"
// );
-//
+//
// Policy 5: Interns may not create new task lists
// forbid (
// principal in Team::"interns",
@@ -48,7 +44,7 @@ when { principal in resource.editors };
// resource == Application::"TinyTodo"
// );
//
-// Policy 6: No access if not high rank and at location DEF,
+// Policy 6: No access if not high rank and at location DEF,
// or at resource's owner's location
// forbid(
// principal,
@@ -57,4 +53,40 @@ when { principal in resource.editors };
// ) unless {
// principal.joblevel > 6 && principal.location like "DEF*" ||
// principal.location == resource.owner.location
-// };
\ No newline at end of file
+// };
+
+// Only team owners can add admins
+permit (
+ principal,
+ action == Action::"AddAdmin",
+ resource
+)
+when { resource.owner == principal };
+
+// Only team owners can remove admins but they can't remove themselves
+permit (
+ principal,
+ action == Action::"RemoveAdmin",
+ resource
+)
+when { resource.owner == principal && context.candidate != principal };
+
+// Only team admins can add members
+permit (
+ principal,
+ action == Action::"AddMember",
+ resource
+)
+when { resource.admins.contains(principal) };
+
+// Only team admins can remove members but they cannot remove other admins
+permit (
+ principal,
+ action == Action::"RemoveMember",
+ resource
+)
+when
+{
+ resource.admins.contains(principal) &&
+ !resource.admins.contains(context.candidate)
+};
\ No newline at end of file
diff --git a/tinytodo/src/api.rs b/tinytodo/src/api.rs
index dc946ec..a15be0e 100644
--- a/tinytodo/src/api.rs
+++ b/tinytodo/src/api.rs
@@ -22,12 +22,111 @@ use warp::Filter;
use crate::{
context::{AppQuery, AppQueryKind, AppResponse, Error},
- objects::{List, TaskState},
- util::{EntityUid, ListUid, UserOrTeamUid, UserUid},
+ objects::{List, TaskState, Team, User},
+ util::{EntityUid, ListUid, TeamUid, UserOrTeamUid, UserUid},
};
type AppChannel = mpsc::Sender;
+#[derive(Debug, Clone, Deserialize)]
+pub struct CreateUser {
+ pub id: String,
+ pub joblevel: i64,
+ pub location: String,
+}
+
+impl From for AppQueryKind {
+ fn from(v: CreateUser) -> AppQueryKind {
+ AppQueryKind::CreateUser(v)
+ }
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct GetUser {
+ pub id: String,
+}
+
+impl From for AppQueryKind {
+ fn from(v: GetUser) -> AppQueryKind {
+ AppQueryKind::GetUser(v)
+ }
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct CreateTeam {
+ pub owner: String,
+ pub id: String,
+}
+
+impl From for AppQueryKind {
+ fn from(v: CreateTeam) -> AppQueryKind {
+ AppQueryKind::CreateTeam(v)
+ }
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct GetTeam {
+ pub uid: TeamUid,
+}
+
+impl From for AppQueryKind {
+ fn from(v: GetTeam) -> AppQueryKind {
+ AppQueryKind::GetTeam(v)
+ }
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct AddAdmin {
+ pub team: TeamUid,
+ pub user: String,
+ pub candidate: String,
+}
+
+impl From for AppQueryKind {
+ fn from(value: AddAdmin) -> Self {
+ AppQueryKind::AddAdmin(value)
+ }
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct RemoveAdmin {
+ pub team: TeamUid,
+ pub user: String,
+ pub candidate: String,
+}
+
+impl From for AppQueryKind {
+ fn from(value: RemoveAdmin) -> Self {
+ AppQueryKind::RemoveAdmin(value)
+ }
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct AddMember {
+ pub team: String,
+ pub user: String,
+ pub candidate: String,
+}
+
+impl From for AppQueryKind {
+ fn from(value: AddMember) -> Self {
+ AppQueryKind::AddMember(value)
+ }
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct RemoveMember {
+ pub team: String,
+ pub user: String,
+ pub candidate: String,
+}
+
+impl From for AppQueryKind {
+ fn from(value: RemoveMember) -> Self {
+ AppQueryKind::RemoveMember(value)
+ }
+}
+
#[derive(Debug, Clone, Deserialize)]
pub struct GetList {
pub uid: UserUid,
@@ -122,6 +221,28 @@ impl From for AppQueryKind {
}
}
+#[derive(Debug, Clone, Deserialize)]
+pub struct GetMemberTeams {
+ pub uid: UserUid,
+}
+
+impl From for AppQueryKind {
+ fn from(v: GetMemberTeams) -> AppQueryKind {
+ AppQueryKind::GetMemberTeams(v)
+ }
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct GetAdminTeams {
+ pub uid: UserUid,
+}
+
+impl From for AppQueryKind {
+ fn from(v: GetAdminTeams) -> AppQueryKind {
+ AppQueryKind::GetAdminTeams(v)
+ }
+}
+
#[derive(Debug, Clone, Deserialize)]
pub struct UpdateTask {
pub uid: UserUid,
@@ -178,26 +299,26 @@ pub async fn serve_api(chan: AppChannel, port: u16) {
let filter = warp::path("api").and(
// List CRUD
(warp::path("list").and(
- (warp::path("get")
+ warp::path("get")
.and(warp::get())
.and(with_app(chan.clone()))
.and(warp::query::query::())
- .and_then(simple_query::))
- .or(warp::path("create")
- .and(warp::post())
- .and(with_app(chan.clone()))
- .and(warp::body::json())
- .and_then(simple_query::))
- .or(warp::path("update")
- .and(warp::post())
- .and(with_app(chan.clone()))
- .and(warp::body::json())
- .and_then(simple_query::))
- .or(warp::path("delete")
- .and(warp::delete())
- .and(with_app(chan.clone()))
- .and(warp::body::json())
- .and_then(simple_query::)),
+ .and_then(simple_query::)
+ .or(warp::path("create")
+ .and(warp::post())
+ .and(with_app(chan.clone()))
+ .and(warp::body::json())
+ .and_then(simple_query::))
+ .or(warp::path("update")
+ .and(warp::post())
+ .and(with_app(chan.clone()))
+ .and(warp::body::json())
+ .and_then(simple_query::))
+ .or(warp::path("delete")
+ .and(warp::delete())
+ .and(with_app(chan.clone()))
+ .and(warp::body::json())
+ .and_then(simple_query::)),
))
.or(
// Task CRUD
@@ -233,6 +354,64 @@ pub async fn serve_api(chan: AppChannel, port: u16) {
.and(with_app(chan.clone()))
.and(warp::body::json())
.and_then(simple_query::)),
+ ))
+ .or(warp::path("user").and(
+ warp::path("create")
+ .and(warp::post())
+ .and(with_app(chan.clone()))
+ .and(warp::body::json())
+ .and_then(simple_query::)
+ .or(warp::path("get")
+ .and(warp::get())
+ .and(with_app(chan.clone()))
+ .and(warp::query::query::())
+ .and_then(simple_query::)),
+ ))
+ .or(warp::path("team").and(
+ (warp::path("create")
+ .and(warp::post())
+ .and(with_app(chan.clone()))
+ .and(warp::body::json())
+ .and_then(simple_query::))
+ .or(warp::path("get")
+ .and(warp::get())
+ .and(with_app(chan.clone()))
+ .and(warp::query::query::())
+ .and_then(simple_query::))
+ .or(warp::path("admin").and(
+ (warp::path("add")
+ .and(warp::post())
+ .and(with_app(chan.clone()))
+ .and(warp::body::json())
+ .and_then(simple_query::))
+ .or(warp::path("remove")
+ .and(warp::delete())
+ .and(with_app(chan.clone()))
+ .and(warp::body::json())
+ .and_then(simple_query::)),
+ ))
+ .or(warp::path("member").and(
+ (warp::path("add")
+ .and(warp::post())
+ .and(with_app(chan.clone()))
+ .and(warp::body::json())
+ .and_then(simple_query::))
+ .or(warp::path("remove")
+ .and(warp::delete())
+ .and(with_app(chan.clone()))
+ .and(warp::body::json())
+ .and_then(simple_query::)),
+ ))
+ .or(warp::path("manage").and(
+ warp::path("member")
+ .and(with_app(chan.clone()))
+ .and(warp::query::query::())
+ .and_then(simple_query::>)
+ .or(warp::path("admin")
+ .and(with_app(chan.clone()))
+ .and(warp::query::query::())
+ .and_then(simple_query::>)),
+ )),
)),
);
@@ -292,7 +471,7 @@ where
let kind = q.into();
let q = AppQuery::new(kind, send);
app.send(q).await?;
- let resp = recv.await??;
+ let resp: AppResponse = recv.await??;
let resp = resp.try_into()?;
Ok(resp)
}
diff --git a/tinytodo/src/context.rs b/tinytodo/src/context.rs
index 8017569..9b97e27 100644
--- a/tinytodo/src/context.rs
+++ b/tinytodo/src/context.rs
@@ -16,12 +16,13 @@
use itertools::Itertools;
use lazy_static::lazy_static;
-use std::path::PathBuf;
+use std::{collections::HashMap, path::PathBuf, str::FromStr};
use tracing::{error, info, trace};
use cedar_policy::{
- Authorizer, Context, Decision, Diagnostics, HumanSchemaError, ParseErrors, PolicySet,
- PolicySetError, Request, Schema, SchemaError, ValidationMode, Validator,
+ Authorizer, Context, Decision, Diagnostics, Entities, EntityId, HumanSchemaError, ParseErrors,
+ PartialResponse, PolicySet, PolicySetError, Request, RequestBuilder, RestrictedExpression,
+ Schema, SchemaError, ValidationMode, Validator,
};
use thiserror::Error;
@@ -32,21 +33,20 @@ use tokio::sync::{
use crate::{
api::{
- AddShare, CreateList, CreateTask, DeleteList, DeleteShare, DeleteTask, Empty, GetList,
- GetLists, UpdateList, UpdateTask,
+ AddAdmin, AddMember, AddShare, CreateList, CreateTask, CreateTeam, CreateUser, DeleteList,
+ DeleteShare, DeleteTask, Empty, GetAdminTeams, GetList, GetLists, GetMemberTeams, GetTeam,
+ GetUser, RemoveAdmin, RemoveMember, UpdateList, UpdateTask,
},
entitystore::{EntityDecodeError, EntityStore},
- objects::List,
+ objects::{List, Team, User},
policy_store,
- util::{EntityUid, ListUid, TYPE_LIST},
+ util::{EntityUid, ListUid, TeamUid, UserUid, TYPE_LIST, TYPE_TEAM, TYPE_USER},
};
#[cfg(feature = "use-templates")]
use crate::{api::ShareRole, util::UserOrTeamUid};
#[cfg(feature = "use-templates")]
use cedar_policy::{PolicyId, SlotId};
-#[cfg(feature = "use-templates")]
-use std::collections::HashMap;
// There's almost certainly a nicer way to do this than having separate `sender` fields
@@ -57,6 +57,9 @@ pub enum AppResponse {
Lists(Vec),
TaskId(i64),
Unit(()),
+ User(User),
+ Team(Team),
+ Teams(Vec),
}
impl AppResponse {
@@ -108,6 +111,26 @@ impl TryInto for AppResponse {
}
}
+impl TryInto for AppResponse {
+ type Error = Error;
+ fn try_into(self) -> std::result::Result {
+ match self {
+ AppResponse::User(u) => Ok(u),
+ _ => Err(Error::Type),
+ }
+ }
+}
+
+impl TryInto for AppResponse {
+ type Error = Error;
+ fn try_into(self) -> std::result::Result {
+ match self {
+ AppResponse::Team(t) => Ok(t),
+ _ => Err(Error::Type),
+ }
+ }
+}
+
impl TryInto> for AppResponse {
type Error = Error;
fn try_into(self) -> std::result::Result, Self::Error> {
@@ -118,6 +141,16 @@ impl TryInto> for AppResponse {
}
}
+impl TryInto> for AppResponse {
+ type Error = Error;
+ fn try_into(self) -> std::result::Result, Self::Error> {
+ match self {
+ AppResponse::Teams(l) => Ok(l),
+ _ => Err(Error::Type),
+ }
+ }
+}
+
#[derive(Debug)]
pub enum AppQueryKind {
// List CRUD
@@ -140,6 +173,20 @@ pub enum AppQueryKind {
// Policy Set Updates
UpdatePolicySet(PolicySet),
+
+ // User CRUD
+ CreateUser(CreateUser),
+ GetUser(GetUser),
+
+ // Team CRUD
+ CreateTeam(CreateTeam),
+ GetTeam(GetTeam),
+ AddAdmin(AddAdmin),
+ RemoveAdmin(RemoveAdmin),
+ AddMember(AddMember),
+ RemoveMember(RemoveMember),
+ GetMemberTeams(GetMemberTeams),
+ GetAdminTeams(GetAdminTeams),
}
#[derive(Debug)]
@@ -178,6 +225,8 @@ pub enum ContextError {
pub enum Error {
#[error("No Such Entity: {0}")]
NoSuchEntity(EntityUid),
+ #[error("Duplicate entity: {0}")]
+ DuplicateEntity(EntityUid),
#[error("Entity Decode Error: {0}")]
EntityDecode(#[from] EntityDecodeError),
#[error("Authorization Denied")]
@@ -217,6 +266,10 @@ lazy_static! {
static ref ACTION_CREATE_LIST: EntityUid = r#"Action::"CreateList""#.parse().unwrap();
static ref ACTION_UPDATE_LIST: EntityUid = r#"Action::"UpdateList""#.parse().unwrap();
static ref ACTION_DELETE_LIST: EntityUid = r#"Action::"DeleteList""#.parse().unwrap();
+ static ref ACTION_ADD_ADMIN: EntityUid = r#"Action::"AddAdmin""#.parse().unwrap();
+ static ref ACTION_REMOVE_ADMIN: EntityUid = r#"Action::"RemoveAdmin""#.parse().unwrap();
+ static ref ACTION_ADD_MEMBER: EntityUid = r#"Action::"AddMember""#.parse().unwrap();
+ static ref ACTION_REMOVE_MEMBER: EntityUid = r#"Action::"RemoveMember""#.parse().unwrap();
}
pub struct AppContext {
@@ -355,6 +408,16 @@ impl AppContext {
AppQueryKind::AddShare(r) => self.add_share(r),
AppQueryKind::DeleteShare(r) => self.delete_share(r),
AppQueryKind::UpdatePolicySet(set) => self.update_policy_set(set),
+ AppQueryKind::CreateUser(r) => self.create_user(r),
+ AppQueryKind::GetUser(r) => self.get_user(r),
+ AppQueryKind::CreateTeam(r) => self.create_team(r),
+ AppQueryKind::GetTeam(r) => self.get_team(r),
+ AppQueryKind::AddAdmin(r) => self.add_admin(r),
+ AppQueryKind::RemoveAdmin(r) => self.remove_admin(r),
+ AppQueryKind::AddMember(r) => self.add_member(r),
+ AppQueryKind::RemoveMember(r) => self.remove_member(r),
+ AppQueryKind::GetAdminTeams(r) => self.get_admin_teams(r),
+ AppQueryKind::GetMemberTeams(r) => self.get_member_teams(r),
};
if let Err(e) = msg.sender.send(r) {
trace!("Failed send response: {:?}", e);
@@ -526,6 +589,330 @@ impl AppContext {
))
}
+ fn reauthorize_with_concrete_resource(
+ &self,
+ partial_response: &PartialResponse,
+ euid: &EntityUid,
+ entities: &Entities,
+ ) -> bool {
+ matches!(
+ partial_response.reauthorize(
+ HashMap::from_iter(
+ std::iter::once(
+ ("resource".into(), RestrictedExpression::new_entity_uid(cedar_policy::EntityUid::from((*euid).clone())))
+ )
+ ),
+ &self.authorizer,
+ &entities),
+ Ok(r) if !matches!(r.decision(), Some(Decision::Deny)))
+ }
+
+ fn get_admin_teams(&self, r: GetAdminTeams) -> Result {
+ let entities = self.entities.as_entities(&self.schema);
+ let partial_request_add = RequestBuilder::default()
+ .action(Some(ACTION_ADD_ADMIN.as_ref().clone().into()))
+ .principal(Some(cedar_policy::EntityUid::from(EntityUid::from(
+ r.uid.clone(),
+ ))))
+ .context(
+ Context::from_pairs(std::iter::once((
+ "candidate".to_owned(),
+ RestrictedExpression::from_str(r#"unknown("candidate")"#).unwrap(),
+ )))
+ .unwrap(),
+ )
+ .build();
+ let partial_request_remove = RequestBuilder::default()
+ .action(Some(ACTION_REMOVE_ADMIN.as_ref().clone().into()))
+ .principal(Some(cedar_policy::EntityUid::from(EntityUid::from(
+ r.uid.clone(),
+ ))))
+ .context(
+ Context::from_pairs(std::iter::once((
+ "candidate".to_owned(),
+ RestrictedExpression::new_entity_uid(
+ cedar_policy::EntityUid::from_type_name_and_id(
+ TYPE_USER.clone(),
+ cedar_policy::EntityId::new(""),
+ ),
+ ),
+ )))
+ .unwrap(),
+ )
+ .build();
+ let partial_response_add =
+ self.authorizer
+ .is_authorized_partial(&partial_request_add, &self.policies, &entities);
+ let partial_response_remove = self.authorizer.is_authorized_partial(
+ &partial_request_remove,
+ &self.policies,
+ &entities,
+ );
+
+ let slice = self.entities.get_teams();
+
+ Ok(AppResponse::Teams(
+ slice
+ .filter(|t| {
+ self.reauthorize_with_concrete_resource(
+ &partial_response_add,
+ &t.uid().clone().into(),
+ &entities,
+ ) || self.reauthorize_with_concrete_resource(
+ &partial_response_remove,
+ &t.uid().clone().into(),
+ &entities,
+ )
+ })
+ .cloned()
+ .collect::>(),
+ ))
+ }
+
+ fn get_member_teams(&self, r: GetMemberTeams) -> Result {
+ let entities = self.entities.as_entities(&self.schema);
+ let partial_request_add = RequestBuilder::default()
+ .action(Some(ACTION_ADD_MEMBER.as_ref().clone().into()))
+ .principal(Some(cedar_policy::EntityUid::from(EntityUid::from(
+ r.uid.clone(),
+ ))))
+ .context(
+ Context::from_pairs(std::iter::once((
+ "candidate".to_owned(),
+ RestrictedExpression::from_str(r#"unknown("candidate")"#).unwrap(),
+ )))
+ .unwrap(),
+ )
+ .build();
+ let partial_request_remove = RequestBuilder::default()
+ .action(Some(ACTION_REMOVE_MEMBER.as_ref().clone().into()))
+ .principal(Some(cedar_policy::EntityUid::from(EntityUid::from(
+ r.uid.clone(),
+ ))))
+ .context(
+ Context::from_pairs(std::iter::once((
+ "candidate".to_owned(),
+ RestrictedExpression::new_entity_uid(
+ cedar_policy::EntityUid::from_type_name_and_id(
+ TYPE_USER.clone(),
+ cedar_policy::EntityId::new(""),
+ ),
+ ),
+ )))
+ .unwrap(),
+ )
+ .build();
+ let partial_response_add =
+ self.authorizer
+ .is_authorized_partial(&partial_request_add, &self.policies, &entities);
+ let partial_response_remove = self.authorizer.is_authorized_partial(
+ &partial_request_remove,
+ &self.policies,
+ &entities,
+ );
+
+ let slice = self.entities.get_teams();
+
+ Ok(AppResponse::Teams(
+ slice
+ .filter(|t| {
+ self.reauthorize_with_concrete_resource(
+ &partial_response_add,
+ &t.uid().clone().into(),
+ &entities,
+ ) || self.reauthorize_with_concrete_resource(
+ &partial_response_remove,
+ &t.uid().clone().into(),
+ &entities,
+ )
+ })
+ .cloned()
+ .collect::>(),
+ ))
+ }
+
+ fn create_team(&mut self, r: CreateTeam) -> Result {
+ let u = UserUid::try_from(EntityUid::from(
+ cedar_policy::EntityUid::from_type_name_and_id(
+ TYPE_USER.clone(),
+ EntityId::new(r.owner),
+ ),
+ ))
+ .unwrap();
+ let t = self
+ .entities
+ .fresh_euid::(TYPE_TEAM.clone())
+ .unwrap();
+ self.entities.insert_team(Team::new_w_name(t, &r.id, u));
+ Ok(AppResponse::Unit(()))
+ }
+
+ fn get_team(&mut self, r: GetTeam) -> Result {
+ let team_id = r.uid;
+ self.entities
+ .get_team(&team_id)
+ .map(|v| AppResponse::Team(v.clone()))
+ }
+
+ fn add_admin(&mut self, r: AddAdmin) -> Result {
+ let team_id = r.team;
+ let user_id = UserUid::try_from(EntityUid::from(
+ cedar_policy::EntityUid::from_type_name_and_id(
+ TYPE_USER.clone(),
+ EntityId::new(r.user),
+ ),
+ ))
+ .unwrap();
+ let candidate_id = UserUid::try_from(EntityUid::from(
+ cedar_policy::EntityUid::from_type_name_and_id(
+ TYPE_USER.clone(),
+ EntityId::new(r.candidate),
+ ),
+ ))
+ .unwrap();
+
+ self.is_authorized_with_context(
+ user_id,
+ &*ACTION_ADD_ADMIN,
+ team_id.clone(),
+ Context::from_pairs([("candidate".to_owned(), candidate_id.clone().into())]).unwrap(),
+ )?;
+
+ self.entities.add_admin(team_id, candidate_id)?;
+
+ Ok(AppResponse::Unit(()))
+ }
+
+ fn remove_member(&mut self, r: RemoveMember) -> Result {
+ let team_id = TeamUid::try_from(EntityUid::from(
+ cedar_policy::EntityUid::from_type_name_and_id(
+ TYPE_TEAM.clone(),
+ EntityId::new(r.team),
+ ),
+ ))
+ .unwrap();
+ let user_id = UserUid::try_from(EntityUid::from(
+ cedar_policy::EntityUid::from_type_name_and_id(
+ TYPE_USER.clone(),
+ EntityId::new(r.user),
+ ),
+ ))
+ .unwrap();
+ let candidate_id = UserUid::try_from(EntityUid::from(
+ cedar_policy::EntityUid::from_type_name_and_id(
+ TYPE_USER.clone(),
+ EntityId::new(r.candidate),
+ ),
+ ))
+ .unwrap();
+
+ self.is_authorized_with_context(
+ user_id,
+ &*ACTION_REMOVE_MEMBER,
+ team_id.clone(),
+ Context::from_pairs([("candidate".to_owned(), candidate_id.clone().into())]).unwrap(),
+ )?;
+
+ self.entities.remove_user_from_team(candidate_id, team_id)?;
+
+ Ok(AppResponse::Unit(()))
+ }
+
+ fn add_member(&mut self, r: AddMember) -> Result {
+ let team_id = TeamUid::try_from(EntityUid::from(
+ cedar_policy::EntityUid::from_type_name_and_id(
+ TYPE_TEAM.clone(),
+ EntityId::new(r.team),
+ ),
+ ))
+ .unwrap();
+ let user_id = UserUid::try_from(EntityUid::from(
+ cedar_policy::EntityUid::from_type_name_and_id(
+ TYPE_USER.clone(),
+ EntityId::new(r.user),
+ ),
+ ))
+ .unwrap();
+ let candidate_id = UserUid::try_from(EntityUid::from(
+ cedar_policy::EntityUid::from_type_name_and_id(
+ TYPE_USER.clone(),
+ EntityId::new(r.candidate),
+ ),
+ ))
+ .unwrap();
+
+ self.is_authorized_with_context(
+ user_id,
+ &*ACTION_ADD_MEMBER,
+ team_id.clone(),
+ Context::from_pairs([("candidate".to_owned(), candidate_id.clone().into())]).unwrap(),
+ )?;
+
+ self.entities.add_user_to_team(candidate_id, team_id)?;
+
+ Ok(AppResponse::Unit(()))
+ }
+
+ fn remove_admin(&mut self, r: RemoveAdmin) -> Result {
+ let team_id = r.team;
+ let user_id = UserUid::try_from(EntityUid::from(
+ cedar_policy::EntityUid::from_type_name_and_id(
+ TYPE_USER.clone(),
+ EntityId::new(r.user),
+ ),
+ ))
+ .unwrap();
+ let candidate_id = UserUid::try_from(EntityUid::from(
+ cedar_policy::EntityUid::from_type_name_and_id(
+ TYPE_USER.clone(),
+ EntityId::new(r.candidate),
+ ),
+ ))
+ .unwrap();
+
+ self.is_authorized_with_context(
+ user_id,
+ &*ACTION_REMOVE_ADMIN,
+ team_id.clone(),
+ Context::from_pairs([("candidate".to_owned(), candidate_id.clone().into())]).unwrap(),
+ )?;
+
+ self.entities.remove_admin(team_id, candidate_id)?;
+
+ Ok(AppResponse::Unit(()))
+ }
+
+ fn create_user(&mut self, r: CreateUser) -> Result {
+ let u = User::new(
+ UserUid::try_from(EntityUid::from(
+ cedar_policy::EntityUid::from_type_name_and_id(
+ TYPE_USER.clone(),
+ EntityId::new(r.id),
+ ),
+ ))
+ .unwrap(),
+ r.joblevel,
+ r.location,
+ );
+ match self.entities.get_user(u.uid()) {
+ Ok(_) => Err(Error::DuplicateEntity(u.uid().clone().into())),
+ Err(_) => {
+ self.entities.insert_user(u);
+ Ok(AppResponse::Unit(()))
+ }
+ }
+ }
+
+ fn get_user(&mut self, r: GetUser) -> Result {
+ let user_id = UserUid::try_from(EntityUid::from(
+ cedar_policy::EntityUid::from_type_name_and_id(TYPE_USER.clone(), EntityId::new(r.id)),
+ ))
+ .unwrap();
+ self.entities
+ .get_user(&user_id)
+ .map(|v| AppResponse::User(v.clone()))
+ }
+
fn create_list(&mut self, r: CreateList) -> Result {
self.is_authorized(&r.uid, &*ACTION_CREATE_LIST, &*APPLICATION_TINY_TODO)?;
@@ -558,6 +945,37 @@ impl AppContext {
Ok(AppResponse::Unit(()))
}
+ #[tracing::instrument(skip_all)]
+ pub fn is_authorized_with_context(
+ &self,
+ principal: impl AsRef,
+ action: impl AsRef,
+ resource: impl AsRef,
+ context: cedar_policy::Context,
+ ) -> Result<()> {
+ let es = self.entities.as_entities(&self.schema);
+ let q = Request::new(
+ Some(principal.as_ref().clone().into()),
+ Some(action.as_ref().clone().into()),
+ Some(resource.as_ref().clone().into()),
+ context,
+ Some(&self.schema),
+ )
+ .map_err(|e| Error::Request(e.to_string()))?;
+ info!(
+ "is_authorized request: principal: {}, action: {}, resource: {}",
+ principal.as_ref(),
+ action.as_ref(),
+ resource.as_ref()
+ );
+ let response = self.authorizer.is_authorized(&q, &self.policies, &es);
+ info!("Auth response: {:?}", response);
+ match response.decision() {
+ Decision::Allow => Ok(()),
+ Decision::Deny => Err(Error::AuthDenied(response.diagnostics().clone())),
+ }
+ }
+
#[tracing::instrument(skip_all)]
pub fn is_authorized(
&self,
diff --git a/tinytodo/src/entitystore.rs b/tinytodo/src/entitystore.rs
index 2b6acbb..2671b06 100644
--- a/tinytodo/src/entitystore.rs
+++ b/tinytodo/src/entitystore.rs
@@ -41,6 +41,10 @@ impl EntityStore {
self.lists.values()
}
+ pub fn get_teams(&self) -> impl Iterator- {
+ self.teams.values()
+ }
+
pub fn euids(&self) -> impl Iterator
- {
self.users
.keys()
@@ -88,6 +92,26 @@ impl EntityStore {
self.lists.insert(e.uid().clone().into(), e);
}
+ pub fn add_admin(&mut self, e: TeamUid, c: UserUid) -> Result<(), Error> {
+ match self.teams.get_mut(&e.clone().into()) {
+ Some(t) => {
+ t.add_admin(c);
+ Ok(())
+ }
+ None => Err(Error::no_such_entity(e)),
+ }
+ }
+
+ pub fn remove_admin(&mut self, e: TeamUid, c: UserUid) -> Result<(), Error> {
+ match self.teams.get_mut(&e.clone().into()) {
+ Some(t) => {
+ t.remove_admin(c);
+ Ok(())
+ }
+ None => Err(Error::no_such_entity(e)),
+ }
+ }
+
pub fn delete_entity(&mut self, e: impl AsRef
) -> Result<(), Error> {
let r = e.as_ref();
if self.users.contains_key(r) {
@@ -155,6 +179,36 @@ impl EntityStore {
.get_mut(euid.as_ref())
.ok_or_else(|| Error::no_such_entity(euid.clone()))
}
+
+ pub fn add_user_to_team(&mut self, candidate: UserUid, team: TeamUid) -> Result<(), Error> {
+ // TODO: `get_team` and `get_mut` should trivially succeed after
+ // successful authorization
+ let _ = self.get_team(&team)?;
+ match self.users.get_mut(&candidate.clone().into()) {
+ Some(user) => {
+ user.insert_parent(team);
+ Ok(())
+ }
+ None => Err(Error::no_such_entity(candidate)),
+ }
+ }
+
+ pub fn remove_user_from_team(
+ &mut self,
+ candidate: UserUid,
+ team: TeamUid,
+ ) -> Result<(), Error> {
+ // TODO: `get_team` and `get_mut` should trivially succeed after
+ // successful authorization
+ let _ = self.get_team(&team)?;
+ match self.users.get_mut(&candidate.clone().into()) {
+ Some(user) => {
+ user.delete_parent(&team);
+ Ok(())
+ }
+ None => Err(Error::no_such_entity(candidate)),
+ }
+ }
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
diff --git a/tinytodo/src/objects.rs b/tinytodo/src/objects.rs
index 872c042..41bb826 100644
--- a/tinytodo/src/objects.rs
+++ b/tinytodo/src/objects.rs
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-use std::collections::HashSet;
+use std::collections::{HashMap, HashSet};
use cedar_policy::{Entity, EvalResult, RestrictedExpression};
use serde::{Deserialize, Serialize};
@@ -115,14 +115,31 @@ impl UserOrTeam for User {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Team {
uid: TeamUid,
+ name: Option,
+ owner: UserUid,
+ admins: HashSet,
parents: HashSet,
}
impl Team {
- pub fn new(euid: TeamUid) -> Team {
+ pub fn new_wo_name(euid: TeamUid, owner: UserUid) -> Team {
+ let parent = Application::default().euid().clone();
+ Self {
+ uid: euid,
+ name: None,
+ owner: owner.clone(),
+ admins: [owner].into_iter().collect(),
+ parents: [parent].into_iter().collect(),
+ }
+ }
+
+ pub fn new_w_name(euid: TeamUid, name: &str, owner: UserUid) -> Team {
let parent = Application::default().euid().clone();
Self {
uid: euid,
+ name: Some(name.to_owned()),
+ owner: owner.clone(),
+ admins: [owner].into_iter().collect(),
parents: [parent].into_iter().collect(),
}
}
@@ -130,15 +147,31 @@ impl Team {
pub fn uid(&self) -> &TeamUid {
&self.uid
}
+
+ pub fn add_admin(&mut self, candidate: UserUid) {
+ self.admins.insert(candidate);
+ }
+
+ pub fn remove_admin(&mut self, candidate: UserUid) {
+ self.admins.remove(&candidate);
+ }
}
impl From for Entity {
fn from(team: Team) -> Entity {
let euid: EntityUid = team.uid.into();
- Entity::new_no_attrs(
+ Entity::new(
euid.into(),
+ HashMap::from_iter([
+ (
+ "admins".to_owned(),
+ RestrictedExpression::new_set(team.admins.into_iter().map(|u| u.into())),
+ ),
+ ("owner".to_owned(), team.owner.into()),
+ ]),
team.parents.into_iter().map(|euid| euid.into()).collect(),
)
+ .unwrap()
}
}
@@ -170,9 +203,9 @@ impl List {
#[cfg(not(feature = "use-templates"))]
{
let readers_uid = store.fresh_euid::(TYPE_TEAM.clone()).unwrap();
- let readers = Team::new(readers_uid.clone());
+ let readers = Team::new_wo_name(readers_uid.clone(), owner.clone());
let writers_uid = store.fresh_euid::(TYPE_TEAM.clone()).unwrap();
- let writers = Team::new(writers_uid.clone());
+ let writers = Team::new_wo_name(writers_uid.clone(), owner.clone());
store.insert_team(readers);
store.insert_team(writers);
Self {
diff --git a/tinytodo/src/util.rs b/tinytodo/src/util.rs
index 51a10b4..f8c30be 100644
--- a/tinytodo/src/util.rs
+++ b/tinytodo/src/util.rs
@@ -255,6 +255,12 @@ impl From for EntityUid {
}
}
+impl From for RestrictedExpression {
+ fn from(value: UserUid) -> Self {
+ RestrictedExpression::new_entity_uid(value.as_ref().clone().into())
+ }
+}
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(try_from = "EntityUid")]
#[serde(into = "EntityUid")]
@@ -268,6 +274,12 @@ impl TryFrom for TeamUid {
}
}
+impl From for RestrictedExpression {
+ fn from(value: TeamUid) -> Self {
+ RestrictedExpression::new_entity_uid(value.as_ref().clone().into())
+ }
+}
+
impl AsRef for TeamUid {
fn as_ref(&self) -> &EntityUid {
&self.0
diff --git a/tinytodo/tinytodo-templates.cedarschema b/tinytodo/tinytodo-templates.cedarschema
index 8f120ae..57f23eb 100644
--- a/tinytodo/tinytodo-templates.cedarschema
+++ b/tinytodo/tinytodo-templates.cedarschema
@@ -5,7 +5,11 @@ type Task = {
};
type Tasks = Set;
-entity Team in [Team, Application];
+entity Team in [Team, Application] {
+ "owner": User,
+ "admins": Set,
+};
+
entity List in [Application] = {
"name": String,
"owner": User,
diff --git a/tinytodo/tinytodo.cedarschema b/tinytodo/tinytodo.cedarschema
index afd6a0d..d5110c9 100644
--- a/tinytodo/tinytodo.cedarschema
+++ b/tinytodo/tinytodo.cedarschema
@@ -17,13 +17,16 @@ entity User in [Team, Application] = {
"joblevel": Long,
"location": String,
};
-entity Team in [Team, Application];
+entity Team in [Team, Application] = {
+ "owner": User,
+ "admins": Set,
+};
action DeleteList, GetList, UpdateList appliesTo {
principal: [User],
resource: [List]
};
-action CreateList, GetLists appliesTo {
+action CreateList, GetLists, CreateTeam appliesTo {
principal: [User],
resource: [Application]
};
@@ -35,3 +38,13 @@ action EditShare appliesTo {
principal: [User],
resource: [List]
};
+
+type Candidate = {
+ "candidate": User,
+};
+action AddAdmin, RemoveAdmin, AddMember, RemoveMember appliesTo {
+ principal: User,
+ resource: Team,
+ context: Candidate,
+};
+
diff --git a/tinytodo/tinytodo.py b/tinytodo/tinytodo.py
index 6002589..3465892 100644
--- a/tinytodo/tinytodo.py
+++ b/tinytodo/tinytodo.py
@@ -239,6 +239,67 @@ def inner(list_of_lists):
return inner
+@web_req("Create User")
+def create_user(_, name, job_level, location):
+ data = {
+ 'id': name,
+ 'joblevel': job_level,
+ 'location': location,
+ }
+ f = lambda _: 'Created User %s' % User(name)
+ return server.post('/api/user/create', data), f
+
+@web_req("Get User")
+def get_user(_, name):
+ f = lambda u: str(u)
+ return server.get('/api/user/get?id=%s' % name), f
+
+@web_req("Create User")
+def create_team(_, name, owner):
+ data = {
+ 'id': name,
+ 'owner': owner,
+ }
+ f = lambda _: 'Created Team %s' % Team(name)
+ return server.post('/api/team/create', data), f
+
+@web_req("Get Team")
+def get_team(_, name):
+ f = lambda u: str(u)
+ return server.get('/api/team/get?id=%s' % name), f
+
+@web_req("Add Team Admin")
+def add_admin(_, team, user, admin):
+ data = {
+ 'team': team,
+ 'user': user,
+ 'candidate': admin,
+ }
+ f = lambda _: 'Add admin {0} to team {1}'.format(User(admin), Team(team))
+ return server.post('/api/team/admin/add', data), f
+
+@web_req("Add Team Member")
+def add_member(_, team, user, admin):
+ data = {
+ 'team': team,
+ 'user': user,
+ 'candidate': admin,
+ }
+ f = lambda _: 'Add member {0} to team {1}'.format(User(admin), Team(team))
+ return server.post('/api/team/member/add', data), f
+
+@web_req("Get Admin Teams")
+def get_admin_teams(_, name):
+ user = User(name)
+ req = server.get('/api/team/manage/admin/get?uid=%s' % user.euid())
+ return req, lambda v: str(v)
+
+@web_req("Get Member Teams")
+def get_member_teams(_, name):
+ user = User(name)
+ req = server.get('/api/team/manage/member/get?uid=%s' % user.euid())
+ return req, lambda v: str(v)
+
@web_req("Create List")
def create_list(user, name):
data = {