aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--base/src/permission.rs46
-rw-r--r--common/src/lib.rs3
-rw-r--r--common/src/user.rs2
-rw-r--r--import/src/main.rs2
-rw-r--r--server/src/import.rs49
-rw-r--r--server/src/routes/api/mod.rs7
-rw-r--r--server/src/routes/stream.rs5
-rw-r--r--server/src/routes/ui/admin/user.rs16
-rw-r--r--server/src/routes/ui/assets.rs7
-rw-r--r--server/src/routes/ui/node.rs5
-rw-r--r--web/script/player/track.ts2
11 files changed, 94 insertions, 50 deletions
diff --git a/base/src/permission.rs b/base/src/permission.rs
index 382a16e..cc0e32c 100644
--- a/base/src/permission.rs
+++ b/base/src/permission.rs
@@ -1,19 +1,24 @@
use crate::CONF;
use anyhow::anyhow;
-use jellycommon::user::{PermissionSet, UserPermission};
+use jellycommon::{
+ user::{PermissionSet, UserPermission},
+ Node,
+};
pub trait PermissionSetExt {
- fn check(&self, perm: &UserPermission) -> bool;
+ fn check_explicit(&self, perm: &UserPermission) -> Option<bool>;
+ fn check(&self, perm: &UserPermission) -> bool {
+ self.check_explicit(perm).unwrap_or(perm.default_value())
+ }
fn assert(&self, perm: &UserPermission) -> Result<(), anyhow::Error>;
}
impl PermissionSetExt for PermissionSet {
- fn check(&self, perm: &UserPermission) -> bool {
- *self
- .0
+ fn check_explicit(&self, perm: &UserPermission) -> Option<bool> {
+ self.0
.get(&perm)
.or(CONF.default_permission_set.0.get(&perm))
- .unwrap_or(&perm.default_value())
+ .map(|v| *v)
}
fn assert(&self, perm: &UserPermission) -> Result<(), anyhow::Error> {
if self.check(perm) {
@@ -25,3 +30,32 @@ impl PermissionSetExt for PermissionSet {
}
}
}
+
+pub trait NodePermissionExt {
+ fn only_if_permitted(self, perms: &PermissionSet) -> Self;
+}
+impl NodePermissionExt for Option<Node> {
+ fn only_if_permitted(self, perms: &PermissionSet) -> Self {
+ self.and_then(|node| {
+ if check_node_permission(perms, &node) {
+ Some(node)
+ } else {
+ None
+ }
+ })
+ }
+}
+fn check_node_permission(perms: &PermissionSet, node: &Node) -> bool {
+ if let Some(v) =
+ perms.check_explicit(&UserPermission::AccessNode(node.public.id.clone().unwrap()))
+ {
+ v
+ } else {
+ for com in node.public.path.clone().into_iter().rev() {
+ if let Some(v) = perms.check_explicit(&UserPermission::AccessNode(com.to_owned())) {
+ return v;
+ }
+ }
+ return false;
+ }
+}
diff --git a/common/src/lib.rs b/common/src/lib.rs
index c425c21..2bde0b9 100644
--- a/common/src/lib.rs
+++ b/common/src/lib.rs
@@ -37,7 +37,8 @@ pub struct NodePrivate {
pub struct NodePublic {
pub kind: NodeKind,
pub title: String,
- #[serde(default)] pub parent: Option<String>,
+ #[serde(default)] pub id: Option<String>,
+ #[serde(default)] pub path: Vec<String>,
#[serde(default)] pub children: Vec<String>,
#[serde(default)] pub tagline: Option<String>,
#[serde(default)] pub description: Option<String>,
diff --git a/common/src/user.rs b/common/src/user.rs
index 8640dc2..b049346 100644
--- a/common/src/user.rs
+++ b/common/src/user.rs
@@ -20,6 +20,7 @@ pub enum UserPermission {
OriginalStream,
Transcode,
ManageUsers,
+ FederatedContent,
GenerateInvite,
AccessNode(String),
}
@@ -38,6 +39,7 @@ impl UserPermission {
impl Display for UserPermission {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&match self {
+ UserPermission::FederatedContent => format!("access to federated content"),
UserPermission::Admin => format!("administrative rights"),
UserPermission::OriginalStream => format!("download of the original media"),
UserPermission::Transcode => format!("transcoding"),
diff --git a/import/src/main.rs b/import/src/main.rs
index 3affabd..57e6b99 100644
--- a/import/src/main.rs
+++ b/import/src/main.rs
@@ -333,7 +333,6 @@ fn main() -> anyhow::Result<()> {
}),
},
public: NodePublic {
- parent: None,
federated: None,
ratings,
description: file_meta
@@ -355,6 +354,7 @@ fn main() -> anyhow::Result<()> {
duration: m.duration,
tracks: m.tracks.clone(),
}),
+ ..Default::default()
},
};
diff --git a/server/src/import.rs b/server/src/import.rs
index d3919b5..ad1d5e9 100644
--- a/server/src/import.rs
+++ b/server/src/import.rs
@@ -27,7 +27,7 @@ pub async fn import(db: &Database, fed: &Federation) -> anyhow::Result<()> {
let permit = IMPORT_SEM.try_acquire()?;
db.node.clear()?;
info!("importing...");
- let (_, errs) = import_path(CONF.library_path.clone(), db, fed, None)
+ let (_, errs) = import_path(CONF.library_path.clone(), db, fed, vec![])
.await
.context("indexing")?;
info!("import completed");
@@ -46,7 +46,7 @@ pub async fn import_path(
path: PathBuf,
db: &Database,
fed: &Federation,
- parent: Option<String>,
+ mut node_path: Vec<String>,
) -> anyhow::Result<(Vec<String>, usize)> {
if path.is_dir() {
let mpath = path.join("directory.json");
@@ -62,14 +62,16 @@ pub async fn import_path(
let identifier = if mpath.exists() {
path.file_name().unwrap().to_str().unwrap().to_string()
} else {
- parent
- .clone()
+ node_path
+ .last()
+ .cloned()
.ok_or(anyhow!("non-root node requires parent"))?
};
+ node_path.push(identifier.clone());
let mut all: FuturesUnordered<_> = children_paths
.into_iter()
- .map(|p| import_path(p.clone(), db, fed, Some(identifier.clone())).map_err(|e| (p, e)))
+ .map(|p| import_path(p.clone(), db, fed, node_path.clone()).map_err(|e| (p, e)))
.collect();
let mut children_ids = Vec::new();
@@ -87,13 +89,14 @@ pub async fn import_path(
}
}
if mpath.exists() {
- let mut data: Node =
+ let mut node: Node =
serde_json::from_reader(File::open(mpath).context("metadata missing")?)?;
- data.public.children = children_ids;
- data.public.parent = parent;
+ node.public.children = children_ids;
+ node.public.path = node_path;
+ node.public.id = Some(identifier.to_owned());
info!("adding {identifier}");
- db.node.insert(&identifier, &data)?;
+ db.node.insert(&identifier, &node)?;
Ok((vec![identifier], errs))
} else {
Ok((children_ids, errs))
@@ -101,8 +104,8 @@ pub async fn import_path(
} else if path.is_file() {
info!("loading {path:?}");
let datafile = File::open(path.clone()).context("cant load metadata")?;
- let mut data: Node = serde_json::from_reader(datafile).context("invalid metadata")?;
- let identifier = data.private.id.clone().unwrap_or_else(|| {
+ let mut node: Node = serde_json::from_reader(datafile).context("invalid metadata")?;
+ let identifier = node.private.id.clone().unwrap_or_else(|| {
path.file_name()
.unwrap()
.to_str()
@@ -112,19 +115,20 @@ pub async fn import_path(
.to_string()
});
- let idents = if let Some(io) = data.private.import.take() {
+ let idents = if let Some(io) = node.private.import.take() {
let session = fed
.get_session(&io.host)
.await
.context("creating session")?;
- import_remote(io, db, &session, identifier.clone(), parent)
+ import_remote(io, db, &session, identifier.clone(), node_path)
.await
.context("federated import")?
} else {
debug!("adding {identifier}");
- data.public.parent = parent;
- db.node.insert(&identifier, &data)?;
+ node.public.path = node_path;
+ node.public.id = Some(identifier.to_owned());
+ db.node.insert(&identifier, &node)?;
vec![identifier]
};
Ok((idents, 0))
@@ -141,7 +145,7 @@ async fn import_remote(
db: &Database,
session: &Arc<Session>,
identifier: String,
- parent: Option<String>,
+ mut node_path: Vec<String>,
) -> anyhow::Result<Vec<String>> {
let _permit = SEM_REMOTE_IMPORT.acquire().await.unwrap();
info!("loading federated node {identifier:?}");
@@ -163,9 +167,7 @@ async fn import_remote(
drop(_permit);
- let child_parent = if flatten {
- parent
- } else {
+ if !flatten {
let mut node = Node {
public: node.clone(),
private: NodePrivate {
@@ -179,7 +181,7 @@ async fn import_remote(
}),
},
};
- node.public.parent = parent;
+ node.public.path = node_path.clone();
node.public.federated = Some(opts.host.clone());
node.public
.children
@@ -187,9 +189,10 @@ async fn import_remote(
.for_each(|c| *c = format!("{}{c}", opts.prefix.clone().unwrap_or(String::new())));
debug!("adding {identifier}");
+ node.public.id = Some(identifier.to_owned());
db.node.insert(&identifier, &node)?;
- Some(opts.id.clone())
- };
+ node_path.push(opts.id.clone());
+ }
let mut children: FuturesUnordered<_> = node
.children
@@ -204,7 +207,7 @@ async fn import_remote(
db,
session,
prefixed,
- child_parent.clone(),
+ node_path.clone(),
)
})
.collect();
diff --git a/server/src/routes/api/mod.rs b/server/src/routes/api/mod.rs
index 23f313f..615c836 100644
--- a/server/src/routes/api/mod.rs
+++ b/server/src/routes/api/mod.rs
@@ -41,13 +41,14 @@ pub fn r_api_account_login(database: &State<Database>, data: Json<LoginForm>) ->
#[get("/api/node_raw/<id>")]
pub fn r_api_node_raw(
- _admin: AdminSession,
+ admin: AdminSession,
database: &State<Database>,
- id: String,
+ id: &str,
) -> MyResult<Json<Node>> {
+ drop(admin);
let node = database
.node
- .get(&id)
+ .get(&id.to_string())
.context("retrieving library node")?
.ok_or(anyhow!("node does not exist"))?;
Ok(Json(node))
diff --git a/server/src/routes/stream.rs b/server/src/routes/stream.rs
index 21575b6..b1248ba 100644
--- a/server/src/routes/stream.rs
+++ b/server/src/routes/stream.rs
@@ -6,7 +6,7 @@
use super::ui::{account::session::Session, error::MyError};
use crate::{database::Database, federation::Federation};
use anyhow::{anyhow, Result};
-use jellybase::CONF;
+use jellybase::{permission::NodePermissionExt, CONF};
use jellycommon::{stream::StreamSpec, MediaSource};
use log::{info, warn};
use rocket::{
@@ -36,7 +36,7 @@ pub async fn r_stream_head(
#[get("/n/<id>/stream?<spec..>")]
pub async fn r_stream(
- _sess: Session,
+ session: Session,
federation: &State<Federation>,
db: &State<Database>,
id: &str,
@@ -46,6 +46,7 @@ pub async fn r_stream(
let node = db
.node
.get(&id.to_string())?
+ .only_if_permitted(&session.user.permissions)
.ok_or(anyhow!("node does not exist"))?;
let source = node
.private
diff --git a/server/src/routes/ui/admin/user.rs b/server/src/routes/ui/admin/user.rs
index e61ec45..42bcfa7 100644
--- a/server/src/routes/ui/admin/user.rs
+++ b/server/src/routes/ui/admin/user.rs
@@ -66,15 +66,15 @@ fn manage_single_user<'a>(
Ok(LayoutPage {
title: "User management".to_string(),
content: markup::new! {
- h1 { "Manage User" }
+ h1 { @format!("{:?}", user.display_name) " (" @user.name ")" }
+ a[href=uri!(r_admin_users())] "Back to the User List"
@FlashDisplay { flash: flash.clone() }
- h2 { @format!("{:?}", user.display_name) " (" @user.name ")" }
form[method="POST", action=uri!(r_admin_remove_user())] {
input[type="text", name="name", value=&user.name, hidden];
- input[type="submit", value="Remove(!)"];
+ input[type="submit", value="Remove user(!)"];
}
- h3 { "Permissions" }
+ h2 { "Permissions" }
@PermissionDisplay { perms: &user.permissions }
form[method="POST", action=uri!(r_admin_user_permission())] {
@@ -90,9 +90,9 @@ fn manage_single_user<'a>(
}
fieldset.perms {
legend { "Permission" }
- label { input[type="radio", name="action", value="unset"]; "Unset" }
- label { input[type="radio", name="action", value="grant"]; "Grant" }
- label { input[type="radio", name="action", value="revoke"]; "Revoke" }
+ label { input[type="radio", name="action", value="unset"]; "Unset" } br;
+ label { input[type="radio", name="action", value="grant"]; "Grant" } br;
+ label { input[type="radio", name="action", value="revoke"]; "Revoke" } br;
}
input[type="submit", value="Update"];
}
@@ -132,7 +132,7 @@ pub enum GrantState {
Unset,
}
-#[post("/admin/update_user_permission", data = "<form>")]
+#[post("/admin/q", data = "<form>")]
pub fn r_admin_user_permission(
session: AdminSession,
database: &State<Database>,
diff --git a/server/src/routes/ui/assets.rs b/server/src/routes/ui/assets.rs
index f88faa4..5789685 100644
--- a/server/src/routes/ui/assets.rs
+++ b/server/src/routes/ui/assets.rs
@@ -8,7 +8,7 @@ use crate::{
routes::ui::{account::session::Session, error::MyError, CacheControlFile},
};
use anyhow::{anyhow, Context};
-use jellybase::AssetLocationExt;
+use jellybase::{AssetLocationExt, permission::NodePermissionExt};
use jellycommon::AssetLocation;
use log::info;
use rocket::{get, http::ContentType, FromFormField, State, UriDisplayQuery};
@@ -25,7 +25,7 @@ pub enum AssetRole {
#[get("/n/<id>/asset?<role>&<width>")]
pub async fn r_item_assets(
- _sess: Session,
+ session: Session,
db: &State<Database>,
id: &str,
role: AssetRole,
@@ -34,13 +34,14 @@ pub async fn r_item_assets(
let node = db
.node
.get(&id.to_string())?
+ .only_if_permitted(&session.user.permissions)
.ok_or(anyhow!("node does not exist"))?;
let mut asset = match role {
AssetRole::Backdrop => node.private.backdrop,
AssetRole::Poster => node.private.poster,
};
if let None = asset {
- if let Some(parent) = &node.public.parent {
+ if let Some(parent) = &node.public.path.last() {
let parent = db.node.get(parent)?.ok_or(anyhow!("node does not exist"))?;
asset = match role {
AssetRole::Backdrop => parent.private.backdrop,
diff --git a/server/src/routes/ui/node.rs b/server/src/routes/ui/node.rs
index 1a906f1..b72ec11 100644
--- a/server/src/routes/ui/node.rs
+++ b/server/src/routes/ui/node.rs
@@ -22,6 +22,7 @@ use crate::{
uri,
};
use anyhow::{anyhow, Context};
+use jellybase::permission::NodePermissionExt;
use jellycommon::{MediaInfo, NodeKind, NodePublic, Rating, SourceTrackKind};
use rocket::{get, serde::json::Json, Either, State};
@@ -39,11 +40,11 @@ pub async fn r_library_node_filter<'a>(
aj: AcceptJson,
filter: NodeFilterSort,
) -> Result<Either<DynLayoutPage<'a>, Json<NodePublic>>, MyError> {
- drop(session);
let node = db
.node
.get(&id.to_string())
.context("retrieving library node")?
+ .only_if_permitted(&session.user.permissions)
.ok_or(anyhow!("node does not exist"))?
.public;
@@ -124,7 +125,7 @@ markup::define! {
}
@if matches!(node.kind, NodeKind::Collection | NodeKind::Channel) {
@if matches!(node.kind, NodeKind::Collection) {
- @if let Some(parent) = &node.parent {
+ @if let Some(parent) = &node.path.last().cloned() {
a.dirup[href=uri!(r_library_node(parent))] { "Go up" }
}
}
diff --git a/web/script/player/track.ts b/web/script/player/track.ts
index 890176c..e2d9d85 100644
--- a/web/script/player/track.ts
+++ b/web/script/player/track.ts
@@ -3,7 +3,7 @@ import { JhlsTrack, TimeRange } from "./jhls.d.ts";
import { BufferRange, Player } from "./player.ts";
import { EncodingProfileExt } from "./profiles.ts";
-const TARGET_BUFFER_DURATION = 5
+const TARGET_BUFFER_DURATION = 10
const MIN_BUFFER_DURATION = 1
export interface AppendRange extends TimeRange { buf: ArrayBuffer, index: number, cb: () => void }