make acls actually do something
This commit is contained in:
parent
f06811c7d6
commit
6bbd874162
14 changed files with 189 additions and 65 deletions
|
@ -540,9 +540,9 @@ async fn main() -> Result<(), Error> {
|
|||
.map(|i| {
|
||||
i.split_once('/').map(|i| {
|
||||
(
|
||||
i.0.parse().expect("invalid item ref"),
|
||||
i.1.parse().expect("invalid item ref"),
|
||||
RelInfo {
|
||||
rel_type: i.1.to_string(),
|
||||
rel_type: i.0.to_string(),
|
||||
key: None,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
- [ ] base blob system
|
||||
- [x] upload/download blobs
|
||||
- [ ] enumerate blobs
|
||||
- [x] enumerate blobs
|
||||
- [ ] garbage collect redacted blobs
|
||||
- [ ] process events
|
||||
- [x] x.file
|
||||
- [x] x.redact
|
||||
- [ ] x.user
|
||||
- [ ] x.acl
|
||||
- [x] x.acl
|
||||
- [x] x.tag.local
|
||||
- [ ] x.tag
|
||||
- [ ] x.annotate
|
||||
|
@ -19,6 +19,11 @@
|
|||
- [x] by tags
|
||||
- [x] by relations
|
||||
- [x] by sender
|
||||
- [ ] decentralization
|
||||
- [-] other servers can get events
|
||||
- [ ] other servers can query relations
|
||||
- [ ] other servers can watch for relations
|
||||
- [ ] works through dht
|
||||
- [-] misc
|
||||
- [x] files as blobs
|
||||
- [x] file thumbnails
|
||||
|
|
|
@ -13,7 +13,7 @@ type Role = String;
|
|||
pub struct Acl {
|
||||
pub roles: HashMap<Role, Permissions>,
|
||||
pub users: HashMap<ActorId, HashSet<Role>>,
|
||||
pub admins: HashSet<ActorId>,
|
||||
pub admins: Vec<ActorId>, // HashSet would be better, but is unordered (bad with canonical json)
|
||||
}
|
||||
|
||||
// #[test]
|
||||
|
@ -33,34 +33,32 @@ impl Acl {
|
|||
self.users.contains_key(user) || self.admins.contains(user)
|
||||
}
|
||||
|
||||
pub fn can_send(&self, user: &ActorId, event: &Event, relations: &Relations) -> bool {
|
||||
pub fn can_send(&self, user: &ActorId, event_type: &str, relations: &Relations) -> bool {
|
||||
// admins can send any kind of event
|
||||
if self.admins.contains(user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if a user doesn't exist, they don't have any permission
|
||||
let Some(roles) = self.users.get(user) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let permissions: Option<Vec<&Permissions>> =
|
||||
roles.iter().map(|r| self.roles.get(r)).collect();
|
||||
let Some(permissions) = permissions else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let permissions: Permissions = permissions.into_iter().flatten().cloned().collect();
|
||||
let permissions: Permissions = roles
|
||||
.iter()
|
||||
.flat_map(|r| self.roles.get(r))
|
||||
.flatten()
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
for (from_event_type, rel_type, to_event_type) in permissions {
|
||||
if from_event_type != event.content.get_type() && from_event_type != "*" {
|
||||
if from_event_type != event_type && from_event_type != "*" {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (item_ref, rel_info) in &event.relations {
|
||||
let Some((target, _)) = relations.get(item_ref) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let valid_type = target.content.get_type() == to_event_type || to_event_type == "*";
|
||||
for (rel_event, rel_info) in relations.values() {
|
||||
let valid_type =
|
||||
rel_event.content.get_type() == to_event_type || to_event_type == "*";
|
||||
let valid_rel = rel_info.rel_type == rel_type || rel_type == "*";
|
||||
if valid_type && valid_rel {
|
||||
return true;
|
||||
|
|
|
@ -102,6 +102,9 @@ pub async fn prepare_special(
|
|||
|
||||
return Ok(DelayedAction::Tag(targets, content.tags.clone()));
|
||||
}
|
||||
EventContent::Acl(_) => {
|
||||
return Ok(DelayedAction::None);
|
||||
}
|
||||
EventContent::Other { event_type, .. } => {
|
||||
if event_type.starts_with("x.") {
|
||||
return Err(Error::Validation("unknown core event"));
|
||||
|
@ -117,6 +120,7 @@ pub async fn prepare_special(
|
|||
}
|
||||
|
||||
pub async fn commit_special(me: &Items, action: &DelayedAction) -> Result<(), Error> {
|
||||
debug!("commit special {:?}", action);
|
||||
match action {
|
||||
DelayedAction::Redact(refs) => {
|
||||
// TODO: garbage collect unreferenced blobs
|
||||
|
@ -173,10 +177,15 @@ pub async fn update_search_index(
|
|||
}
|
||||
EventContent::LocalTag(_) | EventContent::Tag(_) => {
|
||||
for (rel, _) in relations.values() {
|
||||
reindex(me, rel).await?;
|
||||
if let EventContent::File(_) = &rel.content {
|
||||
reindex(me, rel).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => reindex(me, event).await?,
|
||||
EventContent::File(_) => {
|
||||
reindex(me, event).await?;
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::{
|
||||
blobs,
|
||||
items::events::update_search_index,
|
||||
perms::can_send_event,
|
||||
routes::things::{thumbnail::ThumbnailSize, Error},
|
||||
state::{
|
||||
db::{sqlite::Sqlite, Database},
|
||||
|
@ -8,6 +9,7 @@ use crate::{
|
|||
},
|
||||
Relations,
|
||||
};
|
||||
use axum::http::StatusCode;
|
||||
use bytes::Bytes;
|
||||
use events::DelayedAction;
|
||||
use lru::LruCache;
|
||||
|
@ -65,11 +67,19 @@ impl Items {
|
|||
// unsure whether to return rowid (and relations) here...
|
||||
pub async fn begin_event_create(&self, wip: WipEvent) -> Result<WipCreate, Error> {
|
||||
debug!("begin new create");
|
||||
dbg!(wip.to_json());
|
||||
|
||||
if !wip.has_valid_signature() {
|
||||
return Err(Error::Validation("missing or invalid signature"));
|
||||
}
|
||||
|
||||
if !can_send_event(&self.db, &wip).await? {
|
||||
return Err(Error::Http(
|
||||
StatusCode::FORBIDDEN,
|
||||
"you can't send this event".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let item_ref = self.blobs.put(blobs::Item::WipEvent(wip.clone())).await?;
|
||||
let event = Event {
|
||||
id: item_ref.clone(),
|
||||
|
|
|
@ -4,23 +4,29 @@ use std::collections::VecDeque;
|
|||
|
||||
use crate::{
|
||||
items::events::get_relations,
|
||||
state::db::{sqlite::Sqlite, Database},
|
||||
routes::things::Error,
|
||||
state::db::{sqlite::Sqlite, Database, DbItem},
|
||||
Relations,
|
||||
};
|
||||
use tracing::trace;
|
||||
use ufh::{
|
||||
acl::Acl,
|
||||
actor::ActorId,
|
||||
event::{Event, EventContent},
|
||||
event::{Event, EventContent, WipEvent},
|
||||
item::ItemRef,
|
||||
};
|
||||
|
||||
// TODO: find out how to cache this
|
||||
#[async_recursion::async_recursion]
|
||||
#[tracing::instrument(skip_all, fields(event.id, user))]
|
||||
pub async fn can_view_event(db: &Sqlite, event: &Event, user: &ActorId) -> bool {
|
||||
let relations = get_relations(db, event).await.unwrap();
|
||||
|
||||
dbg!(&event.sender, user);
|
||||
|
||||
// event is visible if user has sent the event
|
||||
if &event.sender == user {
|
||||
trace!("event matches because sender == user");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -32,9 +38,22 @@ pub async fn can_view_event(db: &Sqlite, event: &Event, user: &ActorId) -> bool
|
|||
}
|
||||
|
||||
// or if an acl allows a user to view an event
|
||||
get_acl(db, &event.id)
|
||||
.await
|
||||
.is_some_and(|acl| acl.can_view(user))
|
||||
let acl = get_acl(db, &event.id).await;
|
||||
match acl {
|
||||
Some(acl) => {
|
||||
if acl.can_view(user) {
|
||||
trace!("event matches because acl allows it");
|
||||
true
|
||||
} else {
|
||||
trace!("event doesn't match because acl doesn't allow viewing");
|
||||
false
|
||||
}
|
||||
}
|
||||
None => {
|
||||
trace!("event doesn't match because no acl exists");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// async fn is_event_visible_nonrecursive(db: &Sqlite, event: &Event, user: &ActorId) -> bool {
|
||||
|
@ -61,46 +80,90 @@ pub async fn can_view_event(db: &Sqlite, event: &Event, user: &ActorId) -> bool
|
|||
|
||||
struct Context<'a> {
|
||||
relations: &'a Relations,
|
||||
event: &'a Event,
|
||||
event: &'a WipEvent,
|
||||
user: &'a ActorId,
|
||||
}
|
||||
|
||||
pub async fn can_send_event(db: &Sqlite, event: &Event, user: &ActorId) -> bool {
|
||||
let relations = get_relations(db, event).await.unwrap();
|
||||
pub async fn can_send_event(db: &Sqlite, wip: &WipEvent) -> Result<bool, Error> {
|
||||
let Some(relations) = &wip.relations else {
|
||||
trace!("event is sendable because there are no relations");
|
||||
return Ok(true);
|
||||
};
|
||||
|
||||
let rel_ids: Vec<_> = relations.keys().cloned().collect();
|
||||
let rel_events = db.bulk_fetch(&rel_ids, false).await?;
|
||||
let relations = rel_events
|
||||
.into_iter()
|
||||
.map(|(item_ref, item)| {
|
||||
let rel_info = &relations[&item_ref];
|
||||
match item {
|
||||
DbItem::Event(event) => Ok((item_ref, (*event, rel_info.clone()))),
|
||||
DbItem::Blob => Err(Error::Validation(
|
||||
"some relations are to blobs instead of events",
|
||||
)),
|
||||
}
|
||||
})
|
||||
.collect::<Result<Relations, Error>>()?;
|
||||
|
||||
let ctx = Context {
|
||||
relations: &relations,
|
||||
event,
|
||||
user,
|
||||
event: wip,
|
||||
user: &wip.sender,
|
||||
};
|
||||
|
||||
for (rel, _) in relations.values() {
|
||||
if !is_relation_valid(db, rel, &ctx).await {
|
||||
return false;
|
||||
if !is_relation_valid(db, rel, &ctx).await? {
|
||||
trace!("event is invalid because all a relation is invalid");
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
true
|
||||
trace!(
|
||||
"event is valid because all {} relations are valid",
|
||||
relations.len()
|
||||
);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn validate_relations(db: &Sqlite, event: &Event, ctx: &Context<'_>) -> bool {
|
||||
let relations = get_relations(db, event).await.unwrap();
|
||||
async fn validate_relations(db: &Sqlite, event: &Event, ctx: &Context<'_>) -> Result<bool, Error> {
|
||||
let relations = get_relations(db, event).await?;
|
||||
if relations.is_empty() {
|
||||
trace!("relations invalid because it's a root event");
|
||||
return Ok(false);
|
||||
}
|
||||
for (rel, _) in relations.values() {
|
||||
if !is_relation_valid(db, rel, ctx).await {
|
||||
return false;
|
||||
if !is_relation_valid(db, rel, ctx).await? {
|
||||
trace!("relation is invalid because a (sub)relation is invalid");
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
true
|
||||
trace!(
|
||||
"relation is valid because all {} (sub)relations are valid",
|
||||
relations.len()
|
||||
);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[async_recursion::async_recursion]
|
||||
async fn is_relation_valid(db: &Sqlite, relation: &Event, ctx: &Context<'_>) -> bool {
|
||||
async fn is_relation_valid(
|
||||
db: &Sqlite,
|
||||
relation: &Event,
|
||||
ctx: &Context<'_>,
|
||||
) -> Result<bool, Error> {
|
||||
// a relation is allowed if the user has sent that event
|
||||
if &relation.sender == ctx.user {
|
||||
return true;
|
||||
trace!("relation is valid because sender == user");
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if let Some(acl) = get_acl(db, &relation.id).await {
|
||||
// or if an acl set on it allows it
|
||||
acl.can_send(ctx.user, ctx.event, ctx.relations)
|
||||
let valid = acl.can_send(ctx.user, ctx.event.content.get_type(), ctx.relations);
|
||||
if valid {
|
||||
trace!("relation is valid because acl matches");
|
||||
} else {
|
||||
trace!("relation is invalid because acl does not match");
|
||||
}
|
||||
Ok(valid)
|
||||
} else {
|
||||
// or if all of it's relations are also valid
|
||||
validate_relations(db, relation, ctx).await
|
||||
|
@ -109,7 +172,7 @@ async fn is_relation_valid(db: &Sqlite, relation: &Event, ctx: &Context<'_>) ->
|
|||
|
||||
async fn get_acl(db: &Sqlite, item_ref: &ItemRef) -> Option<Acl> {
|
||||
let result = db
|
||||
.query_relations(&[("acl", "acl")], &[item_ref.clone()])
|
||||
.query_relations(&[("x.acl", "acl")], &[item_ref.clone()])
|
||||
.await
|
||||
.ok()?;
|
||||
let (_, event) = result.into_iter().last()?;
|
||||
|
|
|
@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
|||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tracing::debug;
|
||||
use tracing::{debug, trace};
|
||||
use ufh::item::ItemRef;
|
||||
use ufh::{event::Event, query::MatchType};
|
||||
|
||||
|
@ -65,21 +65,35 @@ pub async fn route(
|
|||
.db
|
||||
.query_events(&query, params.limit, params.after.map(|p| p.to_string()))
|
||||
.await?;
|
||||
let has_events = !result.events.is_empty();
|
||||
|
||||
use futures_util::stream::{self, StreamExt};
|
||||
|
||||
let result = crate::state::db::QueryResult {
|
||||
events: stream::iter(result.events)
|
||||
.filter_map(|event| async {
|
||||
can_view_event(&state.db, &event, &user)
|
||||
.await
|
||||
.then_some(event)
|
||||
let visible = can_view_event(&state.db, &event, &user).await;
|
||||
trace!("check if event matches (visible = {})", visible);
|
||||
visible.then_some(event)
|
||||
})
|
||||
.collect()
|
||||
.await,
|
||||
..result
|
||||
};
|
||||
|
||||
if result.events.is_empty() && has_events {
|
||||
// TODO (performance): paginate server side
|
||||
return Ok(QueryResult {
|
||||
events: result.events,
|
||||
relations: None,
|
||||
next: result.next,
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME: send newly visible events and remove newly hidden events
|
||||
// this could be hard to do! events in the past could be made visible,
|
||||
// which messes with rowid. i'd need to either restructure long
|
||||
// polling to work or switch to websockets
|
||||
let result = match (params.timeout, result.events.is_empty()) {
|
||||
(Some(timeout), true) => {
|
||||
debug!("no events, waiting for new ones...");
|
||||
|
|
|
@ -13,6 +13,7 @@ use ufh::item::ItemRef;
|
|||
use super::{Error, Response};
|
||||
use crate::{
|
||||
items::Item,
|
||||
perms::can_view_event,
|
||||
routes::util::{perms, Authenticate},
|
||||
ServerState,
|
||||
};
|
||||
|
@ -43,7 +44,22 @@ pub async fn route(
|
|||
blob,
|
||||
))
|
||||
}
|
||||
Item::Event(event) if auth.level > 0 || event.content.get_type() == "x.file" => {
|
||||
Item::Event(event) => {
|
||||
let is_visible = if let Some(user) = &auth.user {
|
||||
can_view_event(&state.db, &event, user).await
|
||||
} else {
|
||||
event.content.get_type() == "x.file" && !event.is_redacted()
|
||||
};
|
||||
if !is_visible {
|
||||
// TODO (security): should this be "not found" instead of forbidden?
|
||||
// it's not like hashes can be reversed or they can view it anyway, but
|
||||
// it *might* be good to hide information at the cost of some qol?
|
||||
return Err(Error::Http(
|
||||
StatusCode::FORBIDDEN,
|
||||
"you can't view this event".into(),
|
||||
));
|
||||
}
|
||||
|
||||
debug!("got event");
|
||||
let event_str = serde_json::to_string(&event)?;
|
||||
let status = if event.is_redacted() {
|
||||
|
@ -57,10 +73,5 @@ pub async fn route(
|
|||
Bytes::from(event_str.into_bytes()),
|
||||
))
|
||||
}
|
||||
// TODO (security): should this be "not found" instead of forbidden?
|
||||
_ => Err(Error::Http(
|
||||
StatusCode::FORBIDDEN,
|
||||
"you can't view this event".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -266,17 +266,16 @@ impl Database for Sqlite {
|
|||
JOIN events AS events_from ON events_from.ref = relations.ref_from
|
||||
JOIN events AS events_to ON events_to.ref = relations.ref_to
|
||||
LEFT JOIN derived ON derived.ref = events_from.ref
|
||||
WHERE (relations.rel_type, events_from.type) IN (
|
||||
WHERE (events_from.type, relations.rel_type) IN (
|
||||
",
|
||||
);
|
||||
builder.push_tuples(relations, |mut q, tup| {
|
||||
q.push_bind(tup.0.to_owned()).push_bind(tup.1.to_owned());
|
||||
q.push_bind(tup.0).push_bind(tup.1);
|
||||
});
|
||||
builder.push(") AND events_to.ref IN (");
|
||||
let mut sep = builder.separated(",");
|
||||
for item_ref in for_events {
|
||||
let item_ref_str = item_ref.to_string();
|
||||
sep.push_bind(item_ref_str);
|
||||
sep.push_bind(item_ref.to_string());
|
||||
}
|
||||
builder.push(")");
|
||||
debug!("generated sql: {}", builder.sql());
|
||||
|
|
|
@ -37,6 +37,12 @@ extern "pseudocode" fn is_event_sendable(event: &Event, user: &ActorId) -> bool
|
|||
|
||||
fn validate_relations(event: &Event, ctx: &Context) -> bool {
|
||||
let relations = get_relations(event);
|
||||
|
||||
// root events always deny
|
||||
if relations.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
relations
|
||||
.values()
|
||||
.all(|rel| is_relation_valid(rel, ctx))
|
||||
|
|
|
@ -111,10 +111,12 @@ export const api = {
|
|||
async function getPagination(): Promise<string> {
|
||||
if (pagination) return pagination;
|
||||
// TODO: make query.senders default to pubkey
|
||||
const queryReal = {
|
||||
...query,
|
||||
senders: [`%${base64.encode(await ed25519.getPublicKeyAsync(self.key))}`],
|
||||
};
|
||||
// const queryReal = {
|
||||
// ...query,
|
||||
// senders: [`%${base64.encode(await ed25519.getPublicKeyAsync(self.key))}`],
|
||||
// };
|
||||
// FIXME: this lets anyone make any file appear to a person, which isn't good
|
||||
const queryReal = query;
|
||||
|
||||
const res = await fetch(self.baseUrl + "things/query", {
|
||||
method: "POST",
|
||||
|
@ -167,6 +169,7 @@ export const api = {
|
|||
}
|
||||
},
|
||||
async makeEvent(type: string, content: any, relations?: Relations): Promise<Event> {
|
||||
// TODO: cache public key
|
||||
const sender = "%" + base64.encode(await ed25519.getPublicKeyAsync(this.key));
|
||||
const event = { type, content, sender, origin_ts: Date.now() } as any;
|
||||
if (relations) event.relations = relations;
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
const query = api.query({
|
||||
types: ["x.file"],
|
||||
tags: tags.length ? tags : null,
|
||||
relations: [["redact", "x.redact"]],
|
||||
relations: [["x.redact", "redact"]],
|
||||
}, true);
|
||||
return {
|
||||
events: collectEvents(query),
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
|
||||
function loadComments(event) {
|
||||
// TODO: stream relations?
|
||||
const query = api.query({ refs: [event.id], relations: [["comment", "l.forum.comment"]] });
|
||||
const query = api.query({ refs: [event.id], relations: [["l.forum.comment", "comment"]] });
|
||||
let update;
|
||||
const root = { event, children: [] };
|
||||
const treeIndex = new Map();
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { api, ed25519 } from "../lib/api";
|
||||
import { api, base64, ed25519 } from "../lib/api";
|
||||
</script>
|
||||
Server: <input
|
||||
type="text"
|
||||
|
@ -23,3 +23,9 @@ Secret key: <input
|
|||
localStorage.setItem("key", newKey);
|
||||
}}
|
||||
>
|
||||
<br />
|
||||
{#await ed25519.getPublicKeyAsync(api.key)}
|
||||
User id: <input type="text" readonly value="loading...">
|
||||
{:then pubkey}
|
||||
User id: <input type="text" readonly value={"%" + base64.encode(pubkey)}>
|
||||
{/await}
|
||||
|
|
Loading…
Reference in a new issue