Skip to content

Commit

Permalink
rework codebase to use sea_orm instead
Browse files Browse the repository at this point in the history
move entities
rework polar rules
buncha other changes

rework rules and endpoint logic

fix role assn

remove unused imports

fix oso query, result might be error

update deps

super thicc diff
too many changes to document, but basically reworking to use SeaORM
WIP

dumb mistake

WIP

bloop

don't actually need the extra ref

more WIP

don't program when tired

seriously, DONT PROGRAM WHEN TIRED

fix

remove custom-derive, use fieldfilter

WIP

adjust rules to work with new pclasses

impl update_user handler

finish user handlers rewrite

finish rework

whoops
  • Loading branch information
fairingrey committed Feb 19, 2022
1 parent e2f20c0 commit 0434463
Show file tree
Hide file tree
Showing 24 changed files with 758 additions and 506 deletions.
459 changes: 369 additions & 90 deletions Cargo.lock

Large diffs are not rendered by default.

28 changes: 15 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[workspace]
members = [".", "entity"]

[dependencies]
anyhow = "1.0.53"
axum = { version = "0.4.5", features = ["headers", "http2", "multipart"] }
bincode = "1.3.3"
chrono = { version = "0.4.19", features = ["serde"] }
dotenv = "0.15.0"
entity = { path = "entity" }
fieldfilter = "0.1.0"
lazy_static = "1.4.0"
lettre = { version = "0.10.0-rc.4", features = [
"tokio1",
Expand All @@ -32,26 +36,24 @@ redis = { version = "0.21.5", features = [
"connection-manager",
], default-features = false }
regex = "1.5.4"
sea-orm = { version = "0.6.0", features = [
"macros",
"debug-print",
"runtime-tokio-native-tls",
"sqlx-postgres",
], default-features = false }
serde = { version = "1.0.136", features = ["derive"] }
serde_json = "1.0.79"
sqlx = { version = "0.5.10", features = [
"runtime-tokio-native-tls",
"postgres",
"uuid",
"chrono",
"json",
"macros",
] }
thiserror = "1.0.30"
tokio = { version = "1.16.1", features = ["rt-multi-thread", "macros", "sync"] }
tower = "0.4.11"
tokio = { version = "1.17.0", features = ["rt-multi-thread", "macros", "sync"] }
tower = "0.4.12"
tower-http = { version = "0.2.2", features = [
"add-extension",
"trace",
"cors",
] }
tracing = "0.1.30"
tracing-subscriber = { version = "0.3.8", features = ["env-filter"] }
tracing = "0.1.31"
tracing-subscriber = { version = "0.3.9", features = ["env-filter"] }
ulid = { version = "0.5.0", features = ["serde", "uuid"] }
uuid = { version = "0.8.2", features = ["serde", "v4"] }
validator = { version = "0.14.0", features = ["derive"] }
33 changes: 33 additions & 0 deletions Makefile.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[tasks.format]
install_crate = "rustfmt"
command = "cargo"
args = ["fmt", "--", "--emit=files"]

[tasks.clean]
command = "cargo"
args = ["clean"]

[tasks.build]
command = "cargo"
args = ["build"]
dependencies = ["clean"]

[tasks.test]
command = "cargo"
args = ["test"]
dependencies = ["clean"]

[tasks.create-db]
command = "sqlx"
args = ["database", "create"]
workspace = false

[tasks.drop-db]
command = "sqlx"
args = ["database", "drop"]
workspace = false

[tasks.generate-entities]
command = "sea-orm-cli"
args = ["generate", "entity", "-o", "entity/src", "--with-serde", "both"]
workspace = false
28 changes: 28 additions & 0 deletions entity/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

[package]
name = "entity"
version = "0.1.0"
edition = "2021"
publish = false

[lib]
name = "entity"
path = "src/mod.rs"

[dependencies]
bincode = "1.3.3"
oso = { git = "https://github.com/fairingrey/oso", branch = "rust-addl-interface", features = [
"uuid-07",
] }
redis = { version = "0.21.5", features = [
"aio",
"tokio-comp",
"connection-manager",
], default-features = false }
sea-orm = { version = "0.6.0", features = [
"macros",
"debug-print",
"runtime-tokio-native-tls",
"sqlx-postgres",
], default-features = false }
serde = { version = "1.0.136", features = ["derive"] }
File renamed without changes.
7 changes: 7 additions & 0 deletions entity/src/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0
pub mod macros;
pub mod prelude;

pub mod sea_orm_active_enums;
pub mod user_account;
3 changes: 3 additions & 0 deletions entity/src/prelude.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0
pub use super::user_account::Entity as UserAccount;
24 changes: 24 additions & 0 deletions entity/src/sea_orm_active_enums.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0
use oso::PolarClass;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(
Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, PolarClass,
)]
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "user_role")]
pub enum UserRole {
#[sea_orm(string_value = "admin")]
Admin,
#[sea_orm(string_value = "contributor")]
Contributor,
#[sea_orm(string_value = "creator")]
Creator,
#[sea_orm(string_value = "maintainer")]
Maintainer,
#[sea_orm(string_value = "member")]
Member,
#[sea_orm(string_value = "moderator")]
Moderator,
}
41 changes: 41 additions & 0 deletions entity/src/user_account.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0
use super::sea_orm_active_enums::UserRole;
use oso::PolarClass;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize, PolarClass)]
#[sea_orm(table_name = "user_account")]
#[polar(class_name = "User")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
#[polar(attribute)]
pub id: Uuid,
pub created_at: DateTimeWithTimeZone,
pub updated_at: DateTimeWithTimeZone,
#[sea_orm(column_type = "Text")]
#[polar(attribute)]
pub name: String,
#[polar(attribute)]
pub email: String,
#[polar(attribute)]
pub role: UserRole,
#[sea_orm(column_type = "Text")]
pub password: String,
#[polar(attribute)]
pub verified: bool,
}

#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}

impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
panic!("No RelationDef")
}
}

impl ActiveModelBehavior for ActiveModel {}

crate::impl_redis_rv!(Model);
4 changes: 4 additions & 0 deletions migrations/20220211233308_create_user_account.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- Add down migration script here
DROP TABLE IF EXISTS user_account;

DROP TYPE IF EXISTS user_role;
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
-- Add up migration script here
CREATE TYPE role AS ENUM ('admin', 'moderator', 'maintainer', 'creator', 'contributor', 'member');
CREATE TYPE user_role AS ENUM ('admin', 'moderator', 'maintainer', 'creator', 'contributor', 'member');

CREATE TABLE users (
CREATE TABLE user_account (
id UUID PRIMARY KEY NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT current_timestamp,
updated_at TIMESTAMPTZ NOT NULL DEFAULT current_timestamp,
name TEXT NOT NULL,
email VARCHAR(254) NOT NULL,
UNIQUE (name, email),
role role NOT NULL DEFAULT 'member',
role user_role NOT NULL DEFAULT 'member',
password TEXT NOT NULL,
verified BOOLEAN NOT NULL DEFAULT FALSE
);

SELECT manage_updated_at('users');
SELECT manage_updated_at('user_account');
4 changes: 0 additions & 4 deletions migrations/20220211233308_create_users.down.sql

This file was deleted.

53 changes: 22 additions & 31 deletions polar/users.polar
Original file line number Diff line number Diff line change
@@ -1,55 +1,46 @@
# User rules

## admins and mods can read all of a user's fields except passwords
allow_field(user: User, "READ", _other_user: User, field) if
allow_field(user: User, _: Read, _other_user: User, field) if
user.role in [Role::Admin, Role::Moderator] and
field in ["name", "created_at", "updated_at", "role", "email"];
field in ["created_at", "updated_at", "name", "email", "role"];

## users can read all of their own fields except passwords
allow_field(user: User, "READ", other_user: User, field) if
allow_field(user: User, _: Read, other_user: User, field) if
user.id == other_user.id and
field in ["name", "created_at", "updated_at", "role", "email"];
field in ["created_at", "updated_at", "name", "email", "role"];

## anyone can read ids, names, created_at, and role of other users
allow_field(_, "READ", _other_user: User, field: String) if
field in ["name", "created_at", "role"];
## anyone can read names, created_at, and role of other users
allow_field(_, _: Read, _other_user: User, field: String) if
field in ["created_at", "name", "role"];

## admins can change user names or emails
allow_field(user: User, "UPDATE", _other_user: User, field: String) if
## admins can change everything for a user except the password
allow(user: User, update: UpdateUser, _other_user: User) if
user.role = Role::Admin and
field in ["name", "email"];
update.password = nil;

## moderators can do the same but only to other users of role below them
allow_field(user: User, "UPDATE", other_user: User, field: String) if
## they cannot assign roles higher than or equal to themselves
allow(user: User, update: UpdateUser, other_user: User) if
user.role = Role::Moderator and
other_user.role in [Role::Maintainer, Role::Creator, Role::Contributor, Role::Member] and
field in ["name", "email"];
update.role in [Role::Maintainer, Role::Creator, Role::Contributor, Role::Member, nil] and
update.password = nil;

## users can update themselves but not their role
allow(user: User, update: UpdateUser, other_user: User) if
user.id = other_user.id and
changes.role = nil;

## admins can delete other users
allow(user: User, "DELETE", _other_user: User) if
allow(user: User, _: Delete, _other_user: User) if
user.role = Role::Admin;

## moderators can also, but again only to other users of role below them
allow(user: User, "DELETE", other_user: User) if
allow(user: User, _: Delete, other_user: User) if
user.role = Role::Moderator and
other_user.role in [Role::Maintainer, Role::Creator, Role::Contributor, Role::Member];

# role specific stuff

## admins can assign any role
allow_assign_role(user: User, _role: Role) if
user.role = Role::Admin;

## Moderators can only assign roles below them
allow_assign_role(user: User, role: Role) if
user.role = Role::Moderator and
role in [Role::Maintainer, Role::Creator, Role::Contributor, Role::Member];

## users can update themselves but only on certain fields
allow_field(user: User, "UPDATE", other_user: User, field) if
field in ["name", "email", "password"] and
user.id = other_user.id;

## users can delete themselves
allow(user: User, "DELETE", other_user: User) if
allow(user: User, _: Delete, other_user: User) if
user.id = other_user.id;
61 changes: 61 additions & 0 deletions src/actions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//! CRUD action-like resources
use anyhow::Result;
use entity::sea_orm_active_enums::UserRole;
use oso::{Oso, PolarClass};
use serde::Deserialize;
use validator::Validate;

use crate::constants::RE_USERNAME;

/// The "READ" action. Because there is no data pertinent to this action it is a unit struct.
#[derive(Debug, Clone, Copy, PolarClass)]
pub(crate) struct Read;

/// The "DELETE" action. Because there is no data pertinent to this action it is a unit struct.
#[derive(Debug, Clone, Copy, PolarClass)]
pub(crate) struct Delete;

/// The action by which a user is updated. Can be understood as a sort of changeset.
///
/// This struct in particular doubles up for multiple use cases. It's used for PUT `/user/:id` form responses,
/// in authorization rules, and also for updates to the ORM.
#[derive(Debug, Clone, Validate, Deserialize, PolarClass)]
pub(crate) struct UpdateUser {
#[validate(
length(
min = 5,
max = 32,
message = "Minimum length is 5 characters, maximum is 32"
),
regex(
path = "RE_USERNAME",
message = "Can only contain letters, numbers, dashes (-), periods (.), and underscores (_)"
)
)]
#[polar(attribute)]
pub(crate) name: Option<String>,
#[validate(email(message = "Must be a valid email address."))]
#[polar(attribute)]
pub(crate) email: Option<String>,
#[polar(attribute)]
pub(crate) role: Option<UserRole>,
}

/// Attempt to create a new oso instance for managing authorization schemes.
pub(crate) fn try_register_oso() -> Result<Oso> {
let mut oso = Oso::new();

// NOTE: load classes here
oso.register_class(entity::user_account::Model::get_polar_class())?;
oso.register_class(UserRole::get_polar_class())?;

// action classes in this module should be loaded here too
oso.register_class(Read::get_polar_class())?;
oso.register_class(Delete::get_polar_class())?;
oso.register_class(UpdateUser::get_polar_class())?;

// NOTE: load oso rule files here
oso.load_files(vec!["polar/users.polar"])?;

Ok(oso)
}
3 changes: 1 addition & 2 deletions src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use redis::AsyncCommands;
use crate::{
constants::{SESSION_COOKIE_NAME, SESSION_DURATION_SECS, SESSION_KEY_PREFIX},
error::MixiniError,
models::User,
server::State,
};

Expand All @@ -18,7 +17,7 @@ use crate::{
/// with the value being the unprefixed key.
#[derive(Debug)]
pub(crate) enum Auth {
KnownUser(User),
KnownUser(entity::user_account::Model),
UnknownUser,
}

Expand Down
4 changes: 4 additions & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
//! Constants
use lazy_static::lazy_static;
use regex::Regex;

lazy_static! {
pub(crate) static ref DOMAIN: String =
std::env::var("DOMAIN").expect("DOMAIN is not set in env");
pub(crate) static ref RE_USERNAME: Regex = Regex::new(r"^[a-zA-Z0-9\.\-_]+$").unwrap();
pub(crate) static ref RE_PASSWORD: Regex =
Regex::new(r"^[a-zA-Z0-9]*[0-9][a-zA-Z0-9]*$").unwrap();
}

// for authorized sessions
Expand Down
Loading

0 comments on commit 0434463

Please sign in to comment.