use crate::core::Model as _;
use crate::core::{ConflictClause, DeleteQuery, Filter, InsertQuery, Op, SqlValue, WhereExpr};
use crate::sql::sqlx;
#[cfg(feature = "postgres")]
use crate::sql::sqlx::{PgPool, Row};
use crate::sql::Auto;
use crate::Model;
use super::error::TenancyError;
#[derive(Model, Debug, Clone)]
#[rustango(
table = "rustango_roles",
display = "name",
admin(
list_display = "name, description",
search_fields = "name, description",
ordering = "name",
)
)]
pub struct Role {
#[rustango(primary_key)]
pub id: Auto<i64>,
#[rustango(max_length = 150, unique)]
pub name: String,
#[rustango(max_length = 500)]
pub description: String,
#[rustango(default = "'{}'")]
pub data: serde_json::Value,
}
#[derive(Model, Debug, Clone)]
#[rustango(
table = "rustango_role_permissions",
display = "codename",
admin(
list_display = "role_id, codename",
search_fields = "codename",
ordering = "role_id, codename",
)
)]
pub struct RolePermission {
#[rustango(primary_key)]
pub id: Auto<i64>,
pub role_id: i64,
#[rustango(max_length = 100)]
pub codename: String,
}
#[derive(Model, Debug, Clone)]
#[rustango(
table = "rustango_user_roles",
admin(list_display = "user_id, role_id", ordering = "user_id, role_id",)
)]
pub struct UserRole {
#[rustango(primary_key)]
pub id: Auto<i64>,
pub user_id: i64,
pub role_id: i64,
}
#[derive(Model, Debug, Clone)]
#[rustango(
table = "rustango_user_permissions",
display = "codename",
admin(
list_display = "user_id, codename, granted",
search_fields = "codename",
ordering = "user_id, codename",
)
)]
pub struct UserPermission {
#[rustango(primary_key)]
pub id: Auto<i64>,
pub user_id: i64,
#[rustango(max_length = 100)]
pub codename: String,
pub granted: bool,
#[rustango(default = "'{}'")]
pub data: serde_json::Value,
}
const ENSURE_SQL: &str = r#"
CREATE TABLE IF NOT EXISTS "rustango_permissions" (
"id" BIGSERIAL PRIMARY KEY,
"table_name" VARCHAR(150) NOT NULL,
"codename" VARCHAR(100) NOT NULL,
"name" VARCHAR(255) NOT NULL DEFAULT '',
CONSTRAINT "rustango_permissions_uq" UNIQUE ("table_name", "codename")
);
CREATE TABLE IF NOT EXISTS "rustango_roles" (
"id" BIGSERIAL PRIMARY KEY,
"name" VARCHAR(150) NOT NULL,
"description" VARCHAR(500) NOT NULL DEFAULT '',
"data" JSONB NOT NULL DEFAULT '{}',
CONSTRAINT "rustango_roles_name_uq" UNIQUE ("name")
);
ALTER TABLE "rustango_roles"
ADD COLUMN IF NOT EXISTS "data" JSONB NOT NULL DEFAULT '{}';
CREATE TABLE IF NOT EXISTS "rustango_role_permissions" (
"id" BIGSERIAL PRIMARY KEY,
"role_id" BIGINT NOT NULL
REFERENCES "rustango_roles"("id")
ON DELETE CASCADE,
"codename" VARCHAR(100) NOT NULL,
CONSTRAINT "rustango_role_permissions_uq" UNIQUE ("role_id", "codename")
);
CREATE TABLE IF NOT EXISTS "rustango_user_roles" (
"id" BIGSERIAL PRIMARY KEY,
"user_id" BIGINT NOT NULL
REFERENCES "rustango_users"("id")
ON DELETE CASCADE,
"role_id" BIGINT NOT NULL
REFERENCES "rustango_roles"("id")
ON DELETE CASCADE,
CONSTRAINT "rustango_user_roles_uq" UNIQUE ("user_id", "role_id")
);
CREATE TABLE IF NOT EXISTS "rustango_user_permissions" (
"id" BIGSERIAL PRIMARY KEY,
"user_id" BIGINT NOT NULL
REFERENCES "rustango_users"("id")
ON DELETE CASCADE,
"codename" VARCHAR(100) NOT NULL,
"granted" BOOLEAN NOT NULL DEFAULT TRUE,
"data" JSONB NOT NULL DEFAULT '{}',
CONSTRAINT "rustango_user_permissions_uq" UNIQUE ("user_id", "codename")
);
ALTER TABLE "rustango_user_permissions"
ADD COLUMN IF NOT EXISTS "data" JSONB NOT NULL DEFAULT '{}';
ALTER TABLE "rustango_users"
ADD COLUMN IF NOT EXISTS "data" JSONB NOT NULL DEFAULT '{}';
ALTER TABLE "rustango_users"
ADD COLUMN IF NOT EXISTS "password_changed_at" TIMESTAMPTZ NULL;
"#;
const ENSURE_SQL_SQLITE: &str = r#"
CREATE TABLE IF NOT EXISTS "rustango_permissions" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"table_name" TEXT NOT NULL,
"codename" TEXT NOT NULL,
"name" TEXT NOT NULL DEFAULT '',
CONSTRAINT "rustango_permissions_uq" UNIQUE ("table_name", "codename")
);
CREATE TABLE IF NOT EXISTS "rustango_roles" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"description" TEXT NOT NULL DEFAULT '',
"data" TEXT NOT NULL DEFAULT '{}',
CONSTRAINT "rustango_roles_name_uq" UNIQUE ("name")
);
CREATE TABLE IF NOT EXISTS "rustango_role_permissions" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"role_id" INTEGER NOT NULL
REFERENCES "rustango_roles"("id")
ON DELETE CASCADE,
"codename" TEXT NOT NULL,
CONSTRAINT "rustango_role_permissions_uq" UNIQUE ("role_id", "codename")
);
CREATE TABLE IF NOT EXISTS "rustango_user_roles" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"user_id" INTEGER NOT NULL
REFERENCES "rustango_users"("id")
ON DELETE CASCADE,
"role_id" INTEGER NOT NULL
REFERENCES "rustango_roles"("id")
ON DELETE CASCADE,
CONSTRAINT "rustango_user_roles_uq" UNIQUE ("user_id", "role_id")
);
CREATE TABLE IF NOT EXISTS "rustango_user_permissions" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"user_id" INTEGER NOT NULL
REFERENCES "rustango_users"("id")
ON DELETE CASCADE,
"codename" TEXT NOT NULL,
"granted" INTEGER NOT NULL DEFAULT 1,
"data" TEXT NOT NULL DEFAULT '{}',
CONSTRAINT "rustango_user_permissions_uq" UNIQUE ("user_id", "codename")
);
"#;
const ENSURE_SQL_MYSQL: &str = r#"
CREATE TABLE IF NOT EXISTS `rustango_permissions` (
`id` BIGINT AUTO_INCREMENT PRIMARY KEY,
`table_name` VARCHAR(150) NOT NULL,
`codename` VARCHAR(100) NOT NULL,
`name` VARCHAR(255) NOT NULL DEFAULT '',
CONSTRAINT `rustango_permissions_uq` UNIQUE (`table_name`, `codename`)
);
CREATE TABLE IF NOT EXISTS `rustango_roles` (
`id` BIGINT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(150) NOT NULL,
`description` VARCHAR(500) NOT NULL DEFAULT '',
`data` JSON NOT NULL,
CONSTRAINT `rustango_roles_name_uq` UNIQUE (`name`)
);
CREATE TABLE IF NOT EXISTS `rustango_role_permissions` (
`id` BIGINT AUTO_INCREMENT PRIMARY KEY,
`role_id` BIGINT NOT NULL,
`codename` VARCHAR(100) NOT NULL,
CONSTRAINT `rustango_role_permissions_uq` UNIQUE (`role_id`, `codename`),
CONSTRAINT `rustango_role_permissions_fk_role`
FOREIGN KEY (`role_id`) REFERENCES `rustango_roles`(`id`) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS `rustango_user_roles` (
`id` BIGINT AUTO_INCREMENT PRIMARY KEY,
`user_id` BIGINT NOT NULL,
`role_id` BIGINT NOT NULL,
CONSTRAINT `rustango_user_roles_uq` UNIQUE (`user_id`, `role_id`),
CONSTRAINT `rustango_user_roles_fk_user`
FOREIGN KEY (`user_id`) REFERENCES `rustango_users`(`id`) ON DELETE CASCADE,
CONSTRAINT `rustango_user_roles_fk_role`
FOREIGN KEY (`role_id`) REFERENCES `rustango_roles`(`id`) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS `rustango_user_permissions` (
`id` BIGINT AUTO_INCREMENT PRIMARY KEY,
`user_id` BIGINT NOT NULL,
`codename` VARCHAR(100) NOT NULL,
`granted` BOOLEAN NOT NULL DEFAULT TRUE,
`data` JSON NOT NULL,
CONSTRAINT `rustango_user_permissions_uq` UNIQUE (`user_id`, `codename`),
CONSTRAINT `rustango_user_permissions_fk_user`
FOREIGN KEY (`user_id`) REFERENCES `rustango_users`(`id`) ON DELETE CASCADE
);
"#;
#[cfg(feature = "postgres")]
#[deprecated(
since = "0.38.0",
note = "use `cargo run -- migrate` (the bootstrap tenant migration creates these tables per-dialect); this runtime DDL helper is PG-only and predates the bootstrap migrations"
)]
pub async fn ensure_tables(pool: &PgPool) -> Result<(), sqlx::Error> {
for stmt in ENSURE_SQL
.split(';')
.map(str::trim)
.filter(|s| !s.is_empty())
{
sqlx::query(stmt).execute(pool).await?;
}
Ok(())
}
pub async fn ensure_tables_pool(pool: &crate::sql::Pool) -> Result<(), sqlx::Error> {
let dialect = pool.dialect();
let ddl = match dialect.name() {
"sqlite" => ENSURE_SQL_SQLITE,
"mysql" => ENSURE_SQL_MYSQL,
_ => ENSURE_SQL,
};
for stmt in ddl.split(';').map(str::trim).filter(|s| !s.is_empty()) {
crate::sql::raw_execute_pool(pool, stmt, Vec::new())
.await
.map_err(|e| match e {
crate::sql::ExecError::Driver(err) => err,
other => sqlx::Error::Protocol(format!("{other}")),
})?;
}
Ok(())
}
#[cfg(feature = "postgres")]
pub async fn has_perm(uid: i64, codename: &str, pool: &PgPool) -> Result<bool, sqlx::Error> {
has_perm_on(uid, codename, pool).await
}
pub async fn has_perm_pool(
uid: i64,
codename: &str,
pool: &crate::sql::Pool,
) -> Result<bool, TenancyError> {
use sqlx::Row as _;
let dialect = pool.dialect();
let users_t = dialect.quote_ident("rustango_users");
let user_perms_t = dialect.quote_ident("rustango_user_permissions");
let user_roles_t = dialect.quote_ident("rustango_user_roles");
let role_perms_t = dialect.quote_ident("rustango_role_permissions");
let p_uid_a = dialect.placeholder(1);
let p_uid_b = dialect.placeholder(2);
let p_cn_a = dialect.placeholder(3);
let p_uid_c = dialect.placeholder(4);
let p_cn_b = dialect.placeholder(5);
let true_lit = dialect.bool_literal(true);
let false_lit = dialect.bool_literal(false);
let sql = format!(
"SELECT \
COALESCE((SELECT is_superuser FROM {users_t} \
WHERE id = {p_uid_a} AND active = {true_lit}), {false_lit}) AS is_super, \
(SELECT granted FROM {user_perms_t} \
WHERE user_id = {p_uid_b} AND codename = {p_cn_a}) AS explicit_grant, \
EXISTS(SELECT 1 FROM {user_roles_t} ur \
JOIN {role_perms_t} rp ON rp.role_id = ur.role_id \
WHERE ur.user_id = {p_uid_c} AND rp.codename = {p_cn_b}) AS via_role"
);
let (is_super, explicit_grant, via_role) = match pool {
#[cfg(feature = "postgres")]
crate::sql::Pool::Postgres(pg) => {
let row = sqlx::query(&sql)
.bind(uid)
.bind(uid)
.bind(codename)
.bind(uid)
.bind(codename)
.fetch_one(pg)
.await?;
(
row.try_get::<bool, _>("is_super").unwrap_or(false),
row.try_get::<Option<bool>, _>("explicit_grant")
.unwrap_or(None),
row.try_get::<bool, _>("via_role").unwrap_or(false),
)
}
#[cfg(feature = "mysql")]
crate::sql::Pool::Mysql(my) => {
let row = sqlx::query(&sql)
.bind(uid)
.bind(uid)
.bind(codename)
.bind(uid)
.bind(codename)
.fetch_one(my)
.await?;
let is_super: i64 = row.try_get("is_super").unwrap_or(0);
let explicit: Option<i64> = row.try_get("explicit_grant").unwrap_or(None);
let via_role: i64 = row.try_get("via_role").unwrap_or(0);
(is_super != 0, explicit.map(|v| v != 0), via_role != 0)
}
#[cfg(feature = "sqlite")]
crate::sql::Pool::Sqlite(sq) => {
let row = sqlx::query(&sql)
.bind(uid)
.bind(uid)
.bind(codename)
.bind(uid)
.bind(codename)
.fetch_one(sq)
.await?;
let is_super: i64 = row.try_get("is_super").unwrap_or(0);
let explicit: Option<i64> = row.try_get("explicit_grant").unwrap_or(None);
let via_role: i64 = row.try_get("via_role").unwrap_or(0);
(is_super != 0, explicit.map(|v| v != 0), via_role != 0)
}
};
if is_super {
return Ok(true);
}
if let Some(granted) = explicit_grant {
return Ok(granted);
}
Ok(via_role)
}
#[cfg(feature = "postgres")]
pub async fn has_perm_on<'c, E>(uid: i64, codename: &str, executor: E) -> Result<bool, sqlx::Error>
where
E: sqlx::Executor<'c, Database = sqlx::Postgres>,
{
let row = sqlx::query(
r#"
WITH user_info AS (
SELECT is_superuser
FROM "rustango_users"
WHERE id = $1 AND active = TRUE
),
explicit AS (
SELECT granted
FROM "rustango_user_permissions"
WHERE user_id = $1 AND codename = $2
),
via_role AS (
SELECT 1
FROM "rustango_user_roles" ur
JOIN "rustango_role_permissions" rp
ON rp.role_id = ur.role_id
WHERE ur.user_id = $1 AND rp.codename = $2
LIMIT 1
)
SELECT
COALESCE((SELECT is_superuser FROM user_info), FALSE) AS is_super,
(SELECT granted FROM explicit) AS explicit_grant,
EXISTS(SELECT 1 FROM via_role) AS via_role
"#,
)
.bind(uid)
.bind(codename)
.fetch_one(executor)
.await?;
let is_super: bool = row.try_get("is_super").unwrap_or(false);
if is_super {
return Ok(true);
}
let explicit: Option<bool> = row.try_get("explicit_grant").unwrap_or(None);
if let Some(granted) = explicit {
return Ok(granted);
}
Ok(row.try_get("via_role").unwrap_or(false))
}
#[cfg(feature = "postgres")]
pub async fn has_any_perm(
uid: i64,
codenames: &[&str],
pool: &PgPool,
) -> Result<bool, sqlx::Error> {
if codenames.is_empty() {
return Ok(false);
}
let names: Vec<&str> = codenames.to_vec();
let row = sqlx::query(
r#"
WITH user_info AS (
SELECT is_superuser
FROM "rustango_users"
WHERE id = $1 AND active = TRUE
),
denied AS (
SELECT codename
FROM "rustango_user_permissions"
WHERE user_id = $1 AND granted = FALSE AND codename = ANY($2::text[])
),
via_role AS (
SELECT 1
FROM "rustango_user_roles" ur
JOIN "rustango_role_permissions" rp ON rp.role_id = ur.role_id
WHERE ur.user_id = $1
AND rp.codename = ANY($2::text[])
AND rp.codename NOT IN (SELECT codename FROM denied)
LIMIT 1
),
explicit_grant AS (
SELECT 1
FROM "rustango_user_permissions"
WHERE user_id = $1 AND granted = TRUE
AND codename = ANY($2::text[])
AND codename NOT IN (SELECT codename FROM denied)
LIMIT 1
)
SELECT
COALESCE((SELECT is_superuser FROM user_info), FALSE) AS is_super,
EXISTS(SELECT 1 FROM via_role) OR EXISTS(SELECT 1 FROM explicit_grant) AS has_any
"#,
)
.bind(uid)
.bind(names)
.fetch_one(pool)
.await?;
let is_super: bool = row.try_get("is_super").unwrap_or(false);
if is_super {
return Ok(true);
}
Ok(row.try_get("has_any").unwrap_or(false))
}
#[cfg(feature = "postgres")]
pub async fn has_all_perms(
uid: i64,
codenames: &[&str],
pool: &PgPool,
) -> Result<bool, sqlx::Error> {
if codenames.is_empty() {
return Ok(true);
}
let names: Vec<&str> = codenames.to_vec();
let expected = codenames.len() as i64;
let row = sqlx::query(
r#"
WITH user_info AS (
SELECT is_superuser
FROM "rustango_users"
WHERE id = $1 AND active = TRUE
),
denied AS (
SELECT codename
FROM "rustango_user_permissions"
WHERE user_id = $1 AND granted = FALSE AND codename = ANY($2::text[])
),
effective AS (
SELECT rp.codename
FROM "rustango_user_roles" ur
JOIN "rustango_role_permissions" rp ON rp.role_id = ur.role_id
WHERE ur.user_id = $1
AND rp.codename = ANY($2::text[])
AND rp.codename NOT IN (SELECT codename FROM denied)
UNION
SELECT codename
FROM "rustango_user_permissions"
WHERE user_id = $1 AND granted = TRUE
AND codename = ANY($2::text[])
AND codename NOT IN (SELECT codename FROM denied)
)
SELECT
COALESCE((SELECT is_superuser FROM user_info), FALSE) AS is_super,
COUNT(DISTINCT codename) AS matched
FROM effective
"#,
)
.bind(uid)
.bind(names)
.fetch_one(pool)
.await?;
let is_super: bool = row.try_get("is_super").unwrap_or(false);
if is_super {
return Ok(true);
}
let matched: i64 = row.try_get("matched").unwrap_or(0);
Ok(matched == expected)
}
#[cfg(feature = "postgres")]
pub async fn create_role(
name: &str,
description: &str,
pool: &PgPool,
) -> Result<i64, TenancyError> {
let mut role = Role {
id: Auto::default(),
name: name.to_owned(),
description: description.to_owned(),
data: serde_json::Value::Object(serde_json::Map::new()),
};
role.save_on(pool).await?;
Ok(role.id.get().copied().unwrap_or(0))
}
pub async fn create_role_pool(
name: &str,
description: &str,
pool: &crate::sql::Pool,
) -> Result<i64, TenancyError> {
let mut role = Role {
id: Auto::default(),
name: name.to_owned(),
description: description.to_owned(),
data: serde_json::Value::Object(serde_json::Map::new()),
};
role.save_pool(pool).await?;
Ok(role.id.get().copied().unwrap_or(0))
}
pub async fn get_or_create_role_pool(
name: &str,
description: &str,
pool: &crate::sql::Pool,
) -> Result<i64, TenancyError> {
use crate::core::Column as _;
use crate::sql::FetcherPool as _;
let existing: Vec<Role> = Role::objects()
.where_(Role::name.eq(name.to_owned()))
.limit(1)
.fetch_pool(pool)
.await?;
if let Some(r) = existing.into_iter().next() {
return Ok(r.id.get().copied().unwrap_or(0));
}
create_role_pool(name, description, pool).await
}
pub async fn has_any_perm_pool(
uid: i64,
codenames: &[&str],
pool: &crate::sql::Pool,
) -> Result<bool, TenancyError> {
for c in codenames {
if has_perm_pool(uid, c, pool).await? {
return Ok(true);
}
}
Ok(false)
}
pub async fn has_all_perms_pool(
uid: i64,
codenames: &[&str],
pool: &crate::sql::Pool,
) -> Result<bool, TenancyError> {
for c in codenames {
if !has_perm_pool(uid, c, pool).await? {
return Ok(false);
}
}
Ok(true)
}
pub async fn user_roles_pool(
uid: i64,
pool: &crate::sql::Pool,
) -> Result<Vec<(i64, String)>, TenancyError> {
let roles = user_roles_qs_pool(uid, pool).await?;
Ok(roles
.into_iter()
.map(|r| (r.id.get().copied().unwrap_or(0), r.name))
.collect())
}
#[cfg(feature = "postgres")]
pub async fn get_or_create_role(
name: &str,
description: &str,
pool: &PgPool,
) -> Result<i64, TenancyError> {
let row = sqlx::query(
r#"
WITH ins AS (
INSERT INTO "rustango_roles" (name, description, data)
VALUES ($1, $2, '{}')
ON CONFLICT (name) DO NOTHING
RETURNING id
)
SELECT id FROM ins
UNION ALL
SELECT id FROM "rustango_roles" WHERE name = $1
LIMIT 1
"#,
)
.bind(name)
.bind(description)
.fetch_one(pool)
.await?;
Ok(row.try_get::<i64, _>("id").unwrap_or(0))
}
#[cfg(feature = "postgres")]
pub async fn grant_role_perm(
role_id: i64,
codename: &str,
pool: &PgPool,
) -> Result<(), TenancyError> {
let query = InsertQuery {
model: RolePermission::SCHEMA,
columns: vec!["role_id", "codename"],
values: vec![SqlValue::from(role_id), SqlValue::from(codename.to_owned())],
returning: vec![],
on_conflict: Some(ConflictClause::DoNothing),
};
crate::sql::insert(pool, &query).await?;
Ok(())
}
pub async fn grant_role_perm_pool(
role_id: i64,
codename: &str,
pool: &crate::sql::Pool,
) -> Result<(), TenancyError> {
let query = InsertQuery {
model: RolePermission::SCHEMA,
columns: vec!["role_id", "codename"],
values: vec![SqlValue::from(role_id), SqlValue::from(codename.to_owned())],
returning: vec![],
on_conflict: Some(ConflictClause::DoNothing),
};
crate::sql::insert_pool(pool, &query).await?;
Ok(())
}
#[cfg(feature = "postgres")]
pub async fn revoke_role_perm(
role_id: i64,
codename: &str,
pool: &PgPool,
) -> Result<(), TenancyError> {
crate::sql::delete(
pool,
&DeleteQuery {
model: RolePermission::SCHEMA,
where_clause: WhereExpr::and_predicates(vec![
Filter {
column: "role_id",
op: Op::Eq,
value: SqlValue::from(role_id),
},
Filter {
column: "codename",
op: Op::Eq,
value: SqlValue::from(codename),
},
]),
},
)
.await?;
Ok(())
}
pub async fn revoke_role_perm_pool(
role_id: i64,
codename: &str,
pool: &crate::sql::Pool,
) -> Result<(), TenancyError> {
crate::sql::delete_pool(
pool,
&DeleteQuery {
model: RolePermission::SCHEMA,
where_clause: WhereExpr::and_predicates(vec![
Filter {
column: "role_id",
op: Op::Eq,
value: SqlValue::from(role_id),
},
Filter {
column: "codename",
op: Op::Eq,
value: SqlValue::from(codename),
},
]),
},
)
.await?;
Ok(())
}
#[cfg(feature = "postgres")]
pub async fn assign_role(user_id: i64, role_id: i64, pool: &PgPool) -> Result<(), TenancyError> {
let query = InsertQuery {
model: UserRole::SCHEMA,
columns: vec!["user_id", "role_id"],
values: vec![SqlValue::from(user_id), SqlValue::from(role_id)],
returning: vec![],
on_conflict: Some(ConflictClause::DoNothing),
};
crate::sql::insert(pool, &query).await?;
Ok(())
}
pub async fn assign_role_pool(
user_id: i64,
role_id: i64,
pool: &crate::sql::Pool,
) -> Result<(), TenancyError> {
let query = InsertQuery {
model: UserRole::SCHEMA,
columns: vec!["user_id", "role_id"],
values: vec![SqlValue::from(user_id), SqlValue::from(role_id)],
returning: vec![],
on_conflict: Some(ConflictClause::DoNothing),
};
crate::sql::insert_pool(pool, &query).await?;
Ok(())
}
#[cfg(feature = "postgres")]
pub async fn remove_role(user_id: i64, role_id: i64, pool: &PgPool) -> Result<(), TenancyError> {
crate::sql::delete(
pool,
&DeleteQuery {
model: UserRole::SCHEMA,
where_clause: WhereExpr::and_predicates(vec![
Filter {
column: "user_id",
op: Op::Eq,
value: SqlValue::from(user_id),
},
Filter {
column: "role_id",
op: Op::Eq,
value: SqlValue::from(role_id),
},
]),
},
)
.await?;
Ok(())
}
pub async fn remove_role_pool(
user_id: i64,
role_id: i64,
pool: &crate::sql::Pool,
) -> Result<(), TenancyError> {
crate::sql::delete_pool(
pool,
&DeleteQuery {
model: UserRole::SCHEMA,
where_clause: WhereExpr::and_predicates(vec![
Filter {
column: "user_id",
op: Op::Eq,
value: SqlValue::from(user_id),
},
Filter {
column: "role_id",
op: Op::Eq,
value: SqlValue::from(role_id),
},
]),
},
)
.await?;
Ok(())
}
#[cfg(feature = "postgres")]
pub async fn set_user_perm(
user_id: i64,
codename: &str,
granted: bool,
pool: &PgPool,
) -> Result<(), TenancyError> {
let query = InsertQuery {
model: UserPermission::SCHEMA,
columns: vec!["user_id", "codename", "granted", "data"],
values: vec![
SqlValue::from(user_id),
SqlValue::from(codename.to_owned()),
SqlValue::from(granted),
SqlValue::Json(serde_json::json!({})),
],
returning: vec![],
on_conflict: Some(ConflictClause::DoUpdate {
target: vec!["user_id", "codename"],
update_columns: vec!["granted"],
}),
};
crate::sql::insert(pool, &query).await?;
Ok(())
}
pub async fn set_user_perm_pool(
user_id: i64,
codename: &str,
granted: bool,
pool: &crate::sql::Pool,
) -> Result<(), TenancyError> {
let query = InsertQuery {
model: UserPermission::SCHEMA,
columns: vec!["user_id", "codename", "granted", "data"],
values: vec![
SqlValue::from(user_id),
SqlValue::from(codename.to_owned()),
SqlValue::from(granted),
SqlValue::Json(serde_json::json!({})),
],
returning: vec![],
on_conflict: Some(ConflictClause::DoUpdate {
target: vec!["user_id", "codename"],
update_columns: vec!["granted"],
}),
};
crate::sql::insert_pool(pool, &query).await?;
Ok(())
}
#[cfg(feature = "postgres")]
pub async fn clear_user_perm(
user_id: i64,
codename: &str,
pool: &PgPool,
) -> Result<(), TenancyError> {
crate::sql::delete(
pool,
&DeleteQuery {
model: UserPermission::SCHEMA,
where_clause: WhereExpr::and_predicates(vec![
Filter {
column: "user_id",
op: Op::Eq,
value: SqlValue::from(user_id),
},
Filter {
column: "codename",
op: Op::Eq,
value: SqlValue::from(codename),
},
]),
},
)
.await?;
Ok(())
}
pub async fn clear_user_perm_pool(
user_id: i64,
codename: &str,
pool: &crate::sql::Pool,
) -> Result<(), TenancyError> {
crate::sql::delete_pool(
pool,
&DeleteQuery {
model: UserPermission::SCHEMA,
where_clause: WhereExpr::and_predicates(vec![
Filter {
column: "user_id",
op: Op::Eq,
value: SqlValue::from(user_id),
},
Filter {
column: "codename",
op: Op::Eq,
value: SqlValue::from(codename),
},
]),
},
)
.await?;
Ok(())
}
pub async fn user_roles_qs_pool(
user_id: i64,
pool: &crate::sql::Pool,
) -> Result<Vec<Role>, sqlx::Error> {
use sqlx::Row as _;
let dialect = pool.dialect();
let roles_t = dialect.quote_ident("rustango_roles");
let user_roles_t = dialect.quote_ident("rustango_user_roles");
let p1 = dialect.placeholder(1);
let sql = format!(
"SELECT r.id, r.name, r.description, r.data \
FROM {roles_t} r \
JOIN {user_roles_t} ur ON ur.role_id = r.id \
WHERE ur.user_id = {p1} \
ORDER BY r.name"
);
let make_role = |id: i64, name: String, description: String, data: serde_json::Value| -> Role {
Role {
id: Auto::Set(id),
name,
description,
data,
}
};
match pool {
#[cfg(feature = "postgres")]
crate::sql::Pool::Postgres(pg) => {
let rows = sqlx::query(&sql).bind(user_id).fetch_all(pg).await?;
rows.iter()
.map(|row| {
Ok(make_role(
row.try_get::<i64, _>("id")?,
row.try_get("name")?,
row.try_get("description")?,
row.try_get::<serde_json::Value, _>("data")
.unwrap_or_else(|_| serde_json::json!({})),
))
})
.collect()
}
#[cfg(feature = "mysql")]
crate::sql::Pool::Mysql(my) => {
let rows = sqlx::query(&sql).bind(user_id).fetch_all(my).await?;
rows.iter()
.map(|row| {
let data: serde_json::Value = row
.try_get::<sqlx::types::Json<serde_json::Value>, _>("data")
.map(|j| j.0)
.unwrap_or_else(|_| serde_json::json!({}));
Ok(make_role(
row.try_get::<i64, _>("id")?,
row.try_get("name")?,
row.try_get("description")?,
data,
))
})
.collect()
}
#[cfg(feature = "sqlite")]
crate::sql::Pool::Sqlite(sq) => {
let rows = sqlx::query(&sql).bind(user_id).fetch_all(sq).await?;
rows.iter()
.map(|row| {
let data: serde_json::Value = row
.try_get::<Option<String>, _>("data")
.ok()
.flatten()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_else(|| serde_json::json!({}));
Ok(make_role(
row.try_get::<i64, _>("id")?,
row.try_get("name")?,
row.try_get("description")?,
data,
))
})
.collect()
}
}
}
pub async fn user_permissions_pool(
uid: i64,
pool: &crate::sql::Pool,
) -> Result<Vec<String>, TenancyError> {
use sqlx::Row as _;
let dialect = pool.dialect();
let user_perms_t = dialect.quote_ident("rustango_user_permissions");
let user_roles_t = dialect.quote_ident("rustango_user_roles");
let role_perms_t = dialect.quote_ident("rustango_role_permissions");
let p1 = dialect.placeholder(1);
let p2 = dialect.placeholder(2);
let p3 = dialect.placeholder(3);
let true_lit = dialect.bool_literal(true);
let false_lit = dialect.bool_literal(false);
let sql = format!(
"WITH denied AS ( \
SELECT codename FROM {user_perms_t} \
WHERE user_id = {p1} AND granted = {false_lit} \
) \
SELECT DISTINCT codename FROM ( \
SELECT rp.codename \
FROM {user_roles_t} ur \
JOIN {role_perms_t} rp ON rp.role_id = ur.role_id \
WHERE ur.user_id = {p2} \
AND rp.codename NOT IN (SELECT codename FROM denied) \
UNION ALL \
SELECT codename FROM {user_perms_t} \
WHERE user_id = {p3} AND granted = {true_lit} \
AND codename NOT IN (SELECT codename FROM denied) \
) effective \
ORDER BY codename"
);
match pool {
#[cfg(feature = "postgres")]
crate::sql::Pool::Postgres(pg) => {
let rows = sqlx::query(&sql)
.bind(uid)
.bind(uid)
.bind(uid)
.fetch_all(pg)
.await?;
Ok(rows
.iter()
.map(|r| r.try_get::<String, _>("codename").unwrap_or_default())
.collect())
}
#[cfg(feature = "mysql")]
crate::sql::Pool::Mysql(my) => {
let rows = sqlx::query(&sql)
.bind(uid)
.bind(uid)
.bind(uid)
.fetch_all(my)
.await?;
Ok(rows
.iter()
.map(|r| r.try_get::<String, _>("codename").unwrap_or_default())
.collect())
}
#[cfg(feature = "sqlite")]
crate::sql::Pool::Sqlite(sq) => {
let rows = sqlx::query(&sql)
.bind(uid)
.bind(uid)
.bind(uid)
.fetch_all(sq)
.await?;
Ok(rows
.iter()
.map(|r| r.try_get::<String, _>("codename").unwrap_or_default())
.collect())
}
}
}
#[cfg(feature = "postgres")]
pub async fn user_roles_qs(user_id: i64, pool: &PgPool) -> Result<Vec<Role>, sqlx::Error> {
let rows = sqlx::query(
r#"SELECT r.id, r.name, r.description
FROM "rustango_roles" r
JOIN "rustango_user_roles" ur ON ur.role_id = r.id
WHERE ur.user_id = $1
ORDER BY r.name"#,
)
.bind(user_id)
.fetch_all(pool)
.await?;
rows.iter()
.map(|row| {
Ok(Role {
id: Auto::Set(row.try_get::<i64, _>("id")?),
name: row.try_get("name")?,
description: row.try_get("description")?,
data: row
.try_get::<serde_json::Value, _>("data")
.unwrap_or_else(|_| serde_json::json!({})),
})
})
.collect()
}
#[cfg(feature = "postgres")]
pub async fn user_roles(uid: i64, pool: &PgPool) -> Result<Vec<(i64, String)>, sqlx::Error> {
let roles = user_roles_qs(uid, pool).await?; Ok(roles
.into_iter()
.map(|r| (r.id.get().copied().unwrap_or(0), r.name))
.collect())
}
#[cfg(feature = "postgres")]
pub async fn user_permissions(uid: i64, pool: &PgPool) -> Result<Vec<String>, TenancyError> {
let rows = sqlx::query(
r#"
WITH denied AS (
SELECT codename
FROM "rustango_user_permissions"
WHERE user_id = $1 AND granted = FALSE
)
SELECT DISTINCT codename
FROM (
SELECT rp.codename
FROM "rustango_user_roles" ur
JOIN "rustango_role_permissions" rp ON rp.role_id = ur.role_id
WHERE ur.user_id = $1
AND rp.codename NOT IN (SELECT codename FROM denied)
UNION ALL
SELECT codename
FROM "rustango_user_permissions"
WHERE user_id = $1 AND granted = TRUE
AND codename NOT IN (SELECT codename FROM denied)
) effective
ORDER BY codename
"#,
)
.bind(uid)
.fetch_all(pool)
.await?;
Ok(rows
.iter()
.map(|r| r.try_get::<String, _>("codename").unwrap_or_default())
.collect())
}
#[must_use]
pub fn model_codenames(table: &str) -> [String; 4] {
[
format!("{table}.add"),
format!("{table}.change"),
format!("{table}.delete"),
format!("{table}.view"),
]
}
#[cfg(feature = "postgres")]
pub async fn auto_create_permissions(pool: &PgPool) -> Result<(), sqlx::Error> {
use crate::core::{inventory, ModelEntry};
let action_names = [
("add", "Can add"),
("change", "Can change"),
("delete", "Can delete"),
("view", "Can view"),
];
let mut tables: Vec<&str> = Vec::new();
let mut codenames: Vec<String> = Vec::new();
let mut names: Vec<String> = Vec::new();
for entry in inventory::iter::<ModelEntry> {
if !entry.schema.permissions {
continue;
}
let table = entry.schema.table;
let model_name = entry.schema.name;
for (action, verb) in &action_names {
tables.push(table);
codenames.push(format!("{table}.{action}"));
names.push(format!("{verb} {model_name}"));
}
}
if tables.is_empty() {
return Ok(());
}
sqlx::query(
r#"INSERT INTO "rustango_permissions" (table_name, codename, name)
SELECT * FROM UNNEST($1::text[], $2::text[], $3::text[])
ON CONFLICT (table_name, codename) DO NOTHING"#,
)
.bind(&tables)
.bind(&codenames)
.bind(&names)
.execute(pool)
.await?;
Ok(())
}
pub async fn auto_create_permissions_pool(pool: &crate::sql::Pool) -> Result<(), TenancyError> {
use crate::core::{inventory, ModelEntry};
let action_names = [
("add", "Can add"),
("change", "Can change"),
("delete", "Can delete"),
("view", "Can view"),
];
for entry in inventory::iter::<ModelEntry> {
if !entry.schema.permissions {
continue;
}
let table = entry.schema.table;
let model_name = entry.schema.name;
for (action, verb) in &action_names {
let dialect = pool.dialect();
let perm_t = dialect.quote_ident("rustango_permissions");
let table_col = dialect.quote_ident("table_name");
let codename_col = dialect.quote_ident("codename");
let name_col = dialect.quote_ident("name");
let p1 = dialect.placeholder(1);
let p2 = dialect.placeholder(2);
let p3 = dialect.placeholder(3);
let sql = format!(
"INSERT INTO {perm_t} ({table_col}, {codename_col}, {name_col}) \
VALUES ({p1}, {p2}, {p3}) \
ON CONFLICT ({table_col}, {codename_col}) DO NOTHING"
);
let codename = format!("{table}.{action}");
let display = format!("{verb} {model_name}");
crate::sql::raw_execute_pool(
pool,
&sql,
vec![
SqlValue::from(table.to_owned()),
SqlValue::from(codename),
SqlValue::from(display),
],
)
.await
.map_err(|e| {
TenancyError::Validation(format!(
"auto_create_permissions_pool: INSERT for `{table}.{action}` failed: {e}"
))
})?;
}
}
Ok(())
}
#[cfg(test)]
mod admin_config_tests {
use super::*;
use crate::core::Model;
#[test]
fn perm_models_carry_admin_config() {
for (label, schema) in [
("Role", Role::SCHEMA),
("RolePermission", RolePermission::SCHEMA),
("UserRole", UserRole::SCHEMA),
("UserPermission", UserPermission::SCHEMA),
] {
assert!(schema.admin.is_some(), "expected admin config on {label}");
}
}
#[test]
fn perm_models_keep_tenant_scope() {
for schema in [
Role::SCHEMA,
RolePermission::SCHEMA,
UserRole::SCHEMA,
UserPermission::SCHEMA,
] {
assert_eq!(schema.scope, crate::core::ModelScope::Tenant);
}
}
}