beginnings of acl

This commit is contained in:
tezlm 2024-02-14 21:07:24 -08:00
parent bd0b350d11
commit 9100867f51
Signed by: tezlm
GPG key ID: 649733FCD94AFBBA
11 changed files with 275 additions and 53 deletions

24
Cargo.lock generated
View file

@ -421,6 +421,18 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "cli"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"dag-resolve",
"dag-resolve-impls",
"rusqlite",
"serde_json",
]
[[package]]
name = "clipboard-win"
version = "4.5.0"
@ -689,18 +701,6 @@ dependencies = [
"thiserror",
]
[[package]]
name = "dag-resolve-cli"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"dag-resolve",
"dag-resolve-impls",
"rusqlite",
"serde_json",
]
[[package]]
name = "dag-resolve-impls"
version = "0.1.0"

View file

@ -1,5 +1,5 @@
[package]
name = "dag-resolve-cli"
name = "cli"
description = "a basic cli"
version = "0.1.0"
edition = "2021"

View file

@ -127,7 +127,7 @@ impl<R: Resolver> State<R> {
fn open(path: impl AsRef<Path>) -> Result<Opened> {
// restore repo
let db = rusqlite::Connection::open(path)?;
let actor: ActorId =
let _actor: ActorId =
db.query_row("SELECT value FROM _config WHERE key='actor_id'", [], |row| {
row.get(0).map(|s: String| s.parse())
})??;
@ -136,7 +136,6 @@ fn open(path: impl AsRef<Path>) -> Result<Opened> {
[],
|row| row.get(0).map(|s: String| serde_json::from_str(&s)),
)??;
dbg!(&actor, &secret);
let mut q = db.prepare("SELECT json FROM _events")?;
let mut rows = q.query([])?;
@ -154,7 +153,6 @@ fn open(path: impl AsRef<Path>) -> Result<Opened> {
drop(event);
drop(rows);
drop(q);
dbg!(&room);
let db = Rc::new(db);
let store = Database::from_conn(db.clone())
.init(room.get_resolver().get_state_config())?;
@ -175,7 +173,6 @@ fn open(path: impl AsRef<Path>) -> Result<Opened> {
drop(event);
drop(rows);
drop(q);
dbg!(&room);
let db = Rc::new(db);
let store = Database::from_conn(db.clone())
.init(room.get_resolver().get_state_config())?;
@ -226,14 +223,12 @@ fn sync_state<R: Resolver + Debug>(from: &mut State<R>, to: &mut State<R>) -> Re
}
from.room.resolve_state(&mut *from.store);
to.room.resolve_state(&mut *to.store);
dbg!(&from.store, &to.store);
Ok(())
}
fn send_event<R: Resolver + Debug>(state: &mut State<R>, data: &str) -> Result<()> {
state.create_event(dbg!(serde_json::from_str(dbg!(data))?))?;
dbg!(&state.room);
dbg!(&state.room.resolve_state(&mut *state.store));
state.room.resolve_state(&mut *state.store);
Ok(())
}

View file

@ -112,8 +112,8 @@ impl Application for Main {
let mut column = Column::new();
column = column.push(text(format!("{} keys", rows.len())).size(24));
for row in rows {
let key = row.values.get("key").unwrap().as_text().unwrap();
let value = row.values.get("value").unwrap().as_text().unwrap();
let key = row.values.get("key").unwrap().as_string().unwrap();
let value = row.values.get("value").unwrap().as_string().unwrap();
column = column.push(text(format!("{key}: {value}")));
}
column = column.push(row![

View file

@ -5,41 +5,160 @@ use std::cmp::Ordering;
use serde::{Deserialize, Serialize};
use dag_resolve::{
actor::ActorId,
event::{CoreContent, Event, EventId},
proto::table::{
ColumnType, IndexType, State, StateConfig, TableConfig, TableRow, Text,
ColumnType, IndexType, State, StateConfig, Table as _, TableConfig, TableRow,
Text,
},
resolver::{Resolver, ResolverEffect},
resolver::{Command, Resolver, Verification},
};
#[derive(Debug, Default)]
/// A basic key-value store
/// An example of how a basic forum could look like
///
/// This is designed to be the "more fully fledged" example and will try to show off a decent number of features
pub struct ForumResolver;
// TODO: document this better - this is reference for me, i should decide on actual names and make things less ambiguous
// events are "actions", "commands", "updates", or whatever you want to call them
// the resolver, "reducer", "reduxer", etc takes the events and the current state and returns how the new state should be mutated
// the state is currently runtime-defined but it should be possible to statically type it to some degree (similar to eventcontent)
#[derive(Debug, Clone, Serialize, Deserialize)]
/// all possible actions someone could take in a forum
pub enum ForumEventContent {
Post {
subject: Text,
body: Text,
},
/// create a new post
Post { subject: Text, body: Text },
/// reply to an existing post
Reply {
post: EventId,
reference: EventId,
body: Text,
},
/// set configuration
Config {
name: Text,
topic: Text,
acl: ForumRoomAcl,
},
/// Add or remove a member
Member { id: ActorId, acl: ForumMemberAcl },
}
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
pub enum ForumRoomAcl {
#[default]
/// Everyone can send messages by default. Probably a bad idea.
Public,
/// Requires people to have "voice" to send messages.
Voice,
}
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
pub enum ForumMemberAcl {
/// The member is banned/muted (only meaningful in public rooms)
Mute,
#[default]
/// The member is not part of the room
None,
/// The member can send messages (only meaningful in voice rooms)
Voice,
/// The member can configure the room
Operator,
}
fn can_send_event(
room_acl: ForumRoomAcl,
author_acl: ForumMemberAcl,
event: &ForumEventContent,
) -> Verification {
match (room_acl, author_acl, event) {
// operators can do everything
(_, ForumMemberAcl::Operator, _) => Verification::Valid,
// muted users can't do anything
(_, ForumMemberAcl::Mute, _) => Verification::Unauthorized,
// Voice | None users can post in public rooms
(
ForumRoomAcl::Public,
ForumMemberAcl::Voice | ForumMemberAcl::None,
ForumEventContent::Post { .. } | ForumEventContent::Reply { .. },
) => Verification::Valid,
// Voice users can post in voice rooms
(
ForumRoomAcl::Voice,
ForumMemberAcl::Voice,
ForumEventContent::Post { .. } | ForumEventContent::Reply { .. },
) => Verification::Valid,
// otherwise, deny
(_, ForumMemberAcl::None | ForumMemberAcl::Voice, _) => Verification::Unauthorized,
}
}
impl ForumMemberAcl {
fn to_str(&self) -> &str {
match self {
ForumMemberAcl::Mute => "mute",
ForumMemberAcl::None => "none",
ForumMemberAcl::Voice => "voice",
ForumMemberAcl::Operator => "operator",
}
}
fn from_str(s: &str) -> Self {
match s {
"mute" => ForumMemberAcl::Mute,
"none" => ForumMemberAcl::None,
"voice" => ForumMemberAcl::Voice,
"operator" => ForumMemberAcl::Operator,
_ => unreachable!("bad data in database"),
}
}
}
impl ForumRoomAcl {
fn to_str(&self) -> &str {
match self {
ForumRoomAcl::Public => "public",
ForumRoomAcl::Voice => "voice",
}
}
fn from_str(s: &str) -> Self {
match s {
"public" => Self::Public,
"voice" => Self::Voice,
_ => unreachable!("bad data in database"),
}
}
}
// ?
// pub struct ForumState
impl Resolver for ForumResolver {
type EventType = ForumEventContent;
fn resolve<S: State>(&self, _state: &S, event: &Event<Self::EventType>) -> Vec<ResolverEffect> {
fn resolve<S: State>(&self, _state: &S, event: &Event<Self::EventType>) -> Vec<Command> {
let row = match event.content() {
CoreContent::Create(_) => return vec![],
CoreContent::Custom(ForumEventContent::Post { .. }) => ResolverEffect::insert(
CoreContent::Create(_) => Command::insert(
"members",
TableRow::new()
.with("actor", event.author().to_owned())
.with("acl", ForumMemberAcl::Operator.to_str()),
),
CoreContent::Custom(ForumEventContent::Post { .. }) => Command::insert(
"replies",
TableRow::new()
.with("event", event.id().to_owned())
@ -47,16 +166,26 @@ impl Resolver for ForumResolver {
),
CoreContent::Custom(ForumEventContent::Reply {
post, reference, ..
}) => ResolverEffect::insert(
}) => Command::insert(
"replies",
TableRow::new()
.with("event", event.id().to_owned())
.with("post", post.to_owned())
.with("reference", reference.to_owned()),
),
CoreContent::Custom(ForumEventContent::Config { name, topic }) => {
ResolverEffect::insert("config", TableRow::new().with("name", name.to_owned()).with("topic", topic.to_owned()))
}
CoreContent::Custom(ForumEventContent::Config { name, topic, acl }) => Command::insert(
"config",
TableRow::new()
.with("name", name.to_owned())
.with("topic", topic.to_owned())
.with("acl", acl.to_str()),
),
CoreContent::Custom(ForumEventContent::Member { id, acl }) => Command::insert(
"members",
TableRow::new()
.with("actor", id.to_owned())
.with("acl", acl.to_str()),
),
};
vec![row]
}
@ -75,6 +204,10 @@ impl Resolver for ForumResolver {
.with_column("topic", ColumnType::Text)
.with_index("name", IndexType::LookupUnique)
.with_index("topic", IndexType::LookupUnique);
let members = TableConfig::new()
.with_column("actor", ColumnType::Actor)
.with_column("acl", ColumnType::String)
.with_index("actor", IndexType::LookupUnique);
let posts = TableConfig::new()
.with_column("event", ColumnType::Event)
.with_column("time", ColumnType::Integer)
@ -85,9 +218,37 @@ impl Resolver for ForumResolver {
.with_column("parent", ColumnType::Event)
.with_index("post", IndexType::Lookup);
StateConfig::new()
.with_table("posts", posts)
.with_table("config", config)
.with_table("members", members)
.with_table("posts", posts)
.with_table("replies", replies)
.verify()
}
fn verify<S: State>(&self, state: &S, event: &Event<Self::EventType>) -> Verification {
let member_entry = state
.table("members")
.unwrap()
.lookup_optional("actor", event.author().to_owned())
.unwrap()
.and_then(|r| r.values.get("acl").cloned())
.and_then(|d| d.as_string().map(ToOwned::to_owned))
.map(|s| ForumMemberAcl::from_str(&s))
.unwrap_or_default();
let room_entry = state
.table("config")
.unwrap()
.lookup_one("key", "acl")
.unwrap()
.values
.get("acl")
.and_then(|d| d.as_string())
.map(|s| ForumRoomAcl::from_str(s))
.unwrap_or_default();
match event.content() {
CoreContent::Create(_) => panic!("create shouldn't be handled by this"),
CoreContent::Custom(c) => can_send_event(room_entry, member_entry, c),
}
}
}

View file

@ -4,13 +4,15 @@ use serde::{Deserialize, Serialize};
use dag_resolve::{
event::{CoreContent, Event},
proto::table::{ColumnType, IndexType, State, StateConfig, TableConfig, TableRow},
resolver::{Resolver, ResolverEffect},
proto::table::{ColumnType, IndexType, State, StateConfig, Table, TableConfig, TableRow},
resolver::{Command, Resolver, Verification},
};
use std::cmp::Ordering;
#[derive(Debug, Default)]
/// A basic key-value store
///
/// This is designed to be the "extremely basic and minimalistic" example
pub struct KVResolver;
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -27,15 +29,19 @@ impl KVResolver {
impl Resolver for KVResolver {
type EventType = KVEventContent;
fn resolve<S: State>(&self, _state: &S, event: &Event<KVEventContent>) -> Vec<ResolverEffect> {
dbg!(event);
fn resolve<S: State>(&self, _state: &S, event: &Event<KVEventContent>) -> Vec<Command> {
match &event.content() {
CoreContent::Create(_) => vec![],
CoreContent::Create(_) => {
let row = TableRow::new()
.with("key", "owner".to_owned())
.with("value", event.author().to_owned());
vec![Command::insert("meta", row)]
},
CoreContent::Custom(KVEventContent::Set(k, v)) => {
let row = TableRow::new()
.with("key", k.to_owned())
.with("value", v.to_owned());
vec![ResolverEffect::insert("kv", row)]
vec![Command::insert("kv", row)]
}
}
}
@ -49,10 +55,22 @@ impl Resolver for KVResolver {
}
fn get_state_config(&self) -> StateConfig {
let meta = TableConfig::new()
.with_column("key", ColumnType::String)
.with_column("value", ColumnType::String)
.with_index("key", IndexType::LookupUnique);
let table = TableConfig::new()
.with_column("key", ColumnType::String)
.with_column("value", ColumnType::String)
.with_index("key", IndexType::LookupUnique);
StateConfig::new().with_table("kv", table).verify()
StateConfig::new().with_table("meta", meta).with_table("kv", table).verify()
}
fn verify<S: State>(&self, state: &S, event: &Event<Self::EventType>) -> Verification {
let entry = state.table("meta").unwrap().lookup_optional("owner", event.author().to_owned()).unwrap();
match entry {
Some(_) => Verification::Valid,
None => Verification::Unauthorized,
}
}
}

View file

@ -81,6 +81,7 @@ impl table::State for Database {
table::ColumnType::Integer => " INT",
table::ColumnType::Float => " REAL",
table::ColumnType::Event => " TEXT",
table::ColumnType::Actor => " TEXT",
});
}
sql.push_str(");\n");
@ -114,6 +115,7 @@ impl table::Table for Table {
table::ColumnType::Float => {
table::ColumnValue::Float(sql_row.get(column_name.as_str()).unwrap())
}
table::ColumnType::Actor => todo!(),
table::ColumnType::Event => todo!(),
};
map.insert(column_name.to_string(), val);
@ -175,6 +177,7 @@ impl table::Table for Table {
table::ColumnValue::Float(sql_row.get(column_name.as_str()).unwrap())
}
table::ColumnType::Event => todo!(),
table::ColumnType::Actor => todo!(),
};
map.insert(column_name.to_string(), val);
}
@ -242,6 +245,9 @@ impl table::Table for Table {
}
sql.push('?');
match (column_type, row.values.remove(name).unwrap()) {
(table::ColumnType::Text, table::ColumnValue::Text(t)) => {
params.push(Value::Text(t.0))
}
(table::ColumnType::String, table::ColumnValue::String(s)) => {
params.push(Value::Text(s))
}
@ -251,7 +257,13 @@ impl table::Table for Table {
(table::ColumnType::Float, table::ColumnValue::Float(r)) => {
params.push(Value::Real(r))
}
_ => todo!(),
(table::ColumnType::Event, table::ColumnValue::Event(event_id)) => {
params.push(Value::Text(event_id.to_string()))
}
(table::ColumnType::Actor, table::ColumnValue::Actor(actor_id)) => {
params.push(Value::Text(actor_id.to_string()))
}
(column_type, column_value) => todo!("column_type={column_type:?} with value {column_value:?}"),
};
}
sql.push(')');

View file

@ -140,7 +140,7 @@ impl<T: Debug + Serialize + Clone> Event<T> {
// verify signature
let value = serde_json::to_value(&shallow)?;
let data = dbg!(canonical_json::to_string(&value)?);
let data = canonical_json::to_string(&value)?;
self.author.verify(data.as_bytes(), &self.signature)?;
if self.author.get_type() != self.signature.get_type() {
return Err(Error::MismatchedSigner {

View file

@ -9,12 +9,15 @@ pub trait Resolver {
type EventType: Clone + Debug + Serialize + for<'a> Deserialize<'a>;
/// Given a set of ordered events, resolve the final state
fn resolve<S: State>(&self, state: &S, event: &Event<Self::EventType>) -> Vec<ResolverEffect>;
fn resolve<S: State>(&self, state: &S, event: &Event<Self::EventType>) -> Vec<Command>;
/// Given two events, decide which one comes first
/// if Ordering::Equal is returned, the timestamp then event id is used
fn tiebreak(&self, a: &Event<Self::EventType>, b: &Event<Self::EventType>) -> Ordering;
/// Verify if an event can be sent or not
fn verify<S: State>(&self, state: &S, event: &Event<Self::EventType>) -> Verification;
/// TEMP: Get the name/id of this resolver
fn name(&self) -> &str;
@ -22,9 +25,21 @@ pub trait Resolver {
fn get_state_config(&self) -> StateConfig;
}
#[derive(Debug)]
pub enum Verification {
/// This event is valid
Valid,
/// This event's data makes sense, but the sender doesn't have permission to send it
Unauthorized,
/// This event contains invalid data
Invalid,
}
#[derive(Debug)]
/// Effects a resolver can produce
pub enum ResolverEffect {
pub enum Command {
/// Insert a new row into the database
Insert {
table: String,
@ -46,7 +61,7 @@ pub enum ResolverEffect {
// Notify {},
}
impl ResolverEffect {
impl Command {
pub fn insert(table: impl Into<String>, row: TableRow) -> Self {
Self::Insert { table: table.into(), row }
}

View file

@ -3,7 +3,7 @@ use crate::{
event::EventId,
event::{CoreContent, CreateContent, Event, HashType, SignatureType},
proto,
resolver::{sort, Resolver, ResolverEffect},
resolver::{sort, Resolver, Command},
Error, Result,
};
use std::fmt::Debug;
@ -76,7 +76,7 @@ impl<R: Resolver> Room<R> {
let effects = resolver.resolve(state, event);
for effect in effects {
match effect {
ResolverEffect::Insert { table, row } => {
Command::Insert { table, row } => {
state.table(&table).unwrap().insert(row).unwrap();
},
}

View file

@ -6,10 +6,14 @@
//! This is a work in progress; the goal is trying to find a good
//! effort/payoff ratio in the database schema definition.
// TODO: properly type things
// maybe replace TableRow with impl Serialize?
// and make schema a trait instead of builder?
use std::{collections::HashMap, fmt::Debug};
use serde::{Deserialize, Serialize};
use crate::event::EventId;
use crate::{actor::ActorId, event::EventId};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
// TODO: actually implement Text
@ -57,6 +61,12 @@ pub enum ColumnType {
/// event reference
Event,
/// actor reference
Actor,
// TODO: enum?
// Enum(...)
}
#[derive(Debug, Default)]
@ -78,6 +88,7 @@ pub struct IndexConfig {
#[derive(Debug, Default, Clone)]
pub struct TableRow {
pub id: u64,
pub values: HashMap<String, ColumnValue>,
}
@ -97,6 +108,9 @@ pub enum ColumnValue {
/// event reference
Event(EventId),
/// actor reference
Actor(ActorId),
}
#[derive(Debug)]
@ -198,6 +212,12 @@ impl From<EventId> for ColumnValue {
}
}
impl From<ActorId> for ColumnValue {
fn from(value: ActorId) -> Self {
ColumnValue::Actor(value)
}
}
impl From<Text> for ColumnValue {
fn from(value: Text) -> Self {
ColumnValue::Text(value)
@ -206,7 +226,7 @@ impl From<Text> for ColumnValue {
// TODO: was there a macro crate for this?
impl ColumnValue {
pub fn as_text(&self) -> Option<&str> {
pub fn as_string(&self) -> Option<&str> {
match self {
ColumnValue::String(s) => Some(s),
_ => None
@ -244,6 +264,7 @@ impl StateConfig {
IndexType::Ordered => match column {
ColumnType::String | ColumnType::Integer | ColumnType::Float => {}
ColumnType::Event => {} // does topological sorting
ColumnType::Actor => {} // is this allowed?
ColumnType::Text => {
panic!("IndexType::Ordered can not be aplied to ColumnType::Text")
}