Setup: utente admin via web, ruoli e run-context con security-group, onboarding install/uninstall script
Nightly Build / build (push) Failing after 6m12s
Nightly Build / build (push) Failing after 6m12s
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use anyhow::{Result, bail};
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
/// The built-in admin role — immutable from the API.
|
||||
@@ -20,6 +20,81 @@ fn from_raw((id, label, permission_group, attrs, created_at): RawRow) -> Role {
|
||||
Role { id, label, permission_group, attrs, created_at }
|
||||
}
|
||||
|
||||
// ── Typed view over `roles.attrs` (§0.1: role attributes live in free-form JSON,
|
||||
// never per-attribute columns) ────────────────────────────────────────────────
|
||||
|
||||
/// Interface mode a role opts into. `full` unless the role explicitly chooses the
|
||||
/// simplified UI; `admin` is resolved to `full` upstream. Values other than the two
|
||||
/// known ones fall back to `full` (tolerant parse).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum UiMode {
|
||||
#[default]
|
||||
Full,
|
||||
Simple,
|
||||
}
|
||||
|
||||
impl UiMode {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
UiMode::Full => "full",
|
||||
UiMode::Simple => "simple",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed parse of `roles.attrs`. The **single** place that reads the attrs JSON, so
|
||||
/// scattered `serde_json::Value.get(...)` calls don't drift. Tolerant: any parse
|
||||
/// error or missing key yields defaults.
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct RoleAttrs {
|
||||
pub ui_mode: UiMode,
|
||||
/// Security-groups (`tool_permission_groups` ids) this role may use **in addition**
|
||||
/// to its default `permission_group`. The default is always implicitly allowed; the
|
||||
/// effective set is `unique({permission_group} ∪ permission_groups)`.
|
||||
pub permission_groups: Vec<String>,
|
||||
}
|
||||
|
||||
impl RoleAttrs {
|
||||
pub fn from_opt(attrs: &Option<String>) -> RoleAttrs {
|
||||
attrs
|
||||
.as_deref()
|
||||
.and_then(|s| serde_json::from_str(s).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Role {
|
||||
pub fn attrs_parsed(&self) -> RoleAttrs {
|
||||
RoleAttrs::from_opt(&self.attrs)
|
||||
}
|
||||
|
||||
/// The security-groups this role may select: its default first, then any extras
|
||||
/// from `attrs.permission_groups`, deduped.
|
||||
pub fn effective_groups(&self) -> Vec<String> {
|
||||
let mut out = vec![self.permission_group.clone()];
|
||||
for g in self.attrs_parsed().permission_groups {
|
||||
if !out.contains(&g) {
|
||||
out.push(g);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a role may use `group_id` as its session security-group. `admin` holds
|
||||
/// every group by construction; a missing role allows nothing.
|
||||
pub async fn role_allows_group(pool: &SqlitePool, role_id: &str, group_id: &str) -> Result<bool> {
|
||||
if role_id == ADMIN_ROLE_ID {
|
||||
return Ok(true);
|
||||
}
|
||||
match get(pool, role_id).await? {
|
||||
Some(role) => Ok(role.effective_groups().iter().any(|g| g == group_id)),
|
||||
None => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reads ────────────────────────────────────────────────────────────────────
|
||||
|
||||
pub async fn list(pool: &SqlitePool) -> Result<Vec<Role>> {
|
||||
@@ -122,3 +197,72 @@ pub async fn seed_admin(pool: &SqlitePool) -> Result<()> {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn tmp_db(tag: &str) -> String {
|
||||
let dir = std::env::temp_dir().join(format!("skald-roles-{tag}-{}", uuid::Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir.join("system.db").to_str().unwrap().to_string()
|
||||
}
|
||||
|
||||
fn role(permission_group: &str, attrs: Option<&str>) -> Role {
|
||||
Role {
|
||||
id: "member".into(),
|
||||
label: "Member".into(),
|
||||
permission_group: permission_group.into(),
|
||||
attrs: attrs.map(str::to_string),
|
||||
created_at: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn role_attrs_are_tolerant() {
|
||||
// Missing → defaults.
|
||||
let a = RoleAttrs::from_opt(&None);
|
||||
assert_eq!(a.ui_mode, UiMode::Full);
|
||||
assert!(a.permission_groups.is_empty());
|
||||
|
||||
// Populated.
|
||||
let a = RoleAttrs::from_opt(&Some(
|
||||
r#"{"ui_mode":"simple","permission_groups":["ops","research"]}"#.into(),
|
||||
));
|
||||
assert_eq!(a.ui_mode, UiMode::Simple);
|
||||
assert_eq!(a.permission_groups, vec!["ops", "research"]);
|
||||
|
||||
// Malformed JSON → defaults, never an error.
|
||||
let a = RoleAttrs::from_opt(&Some("not json".into()));
|
||||
assert_eq!(a.ui_mode, UiMode::Full);
|
||||
assert!(a.permission_groups.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_groups_prepends_default_and_dedups() {
|
||||
let r = role("default", Some(r#"{"permission_groups":["ops","default","research"]}"#));
|
||||
assert_eq!(r.effective_groups(), vec!["default", "ops", "research"]);
|
||||
|
||||
// No extras → just the default.
|
||||
let r = role("kids", None);
|
||||
assert_eq!(r.effective_groups(), vec!["kids"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn role_allows_group_admin_member_and_unknown() {
|
||||
let pool = crate::db::init_system_pool(&tmp_db("allows")).await.unwrap();
|
||||
|
||||
// admin is seeded by init and allows any group by construction.
|
||||
assert!(role_allows_group(&pool, ADMIN_ROLE_ID, "anything").await.unwrap());
|
||||
|
||||
insert(&pool, "member", "Member", "default", Some(r#"{"permission_groups":["ops"]}"#))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(role_allows_group(&pool, "member", "default").await.unwrap());
|
||||
assert!(role_allows_group(&pool, "member", "ops").await.unwrap());
|
||||
assert!(!role_allows_group(&pool, "member", "research").await.unwrap());
|
||||
|
||||
// An unknown role allows nothing.
|
||||
assert!(!role_allows_group(&pool, "ghost", "default").await.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user