Improve documentation around example setup

Fixes: #38
This commit is contained in:
Nadja Reitzenstein 2022-01-06 18:19:02 +01:00
parent f524079914
commit 0da3213395
6 changed files with 224 additions and 112 deletions

View File

@ -1,81 +1,205 @@
-- { actor_connections = [] : List { _1 : Text, _2 : Text } {- Main configuration file for bffh
{ actor_connections = - ================================
-- Link up machines to actors -
[ { machine = "Testmachine", actor = "Shelly1234" } - In this configuration file you configure almost all parts of how bffh operates, but most importantly:
, { machine = "Another", actor = "Bash" } - * Machines
-- One machine can have as many actors as it wants - * Initiators and Actors
, { machine = "Yetmore", actor = "Bash2" } - * Which Initiators and Actors relate to which machine(s)
, { machine = "Yetmore", actor = "FailBash"} - * Roles and the permissions granted by them
] -}
, actors =
{ Shelly1234 = { module = "Shelly", params = -- The config is in the configuration format/language dhall. You can find more information about dhall over at
{ topic = "Topic1234" }} -- https://dhall-lang.org
, Bash = { module = "Process", params =
{ cmd = "./examples/actor.sh" -- (Our) Dhall is somewhat similar to JSON and YAML in that it expects a top-level object containing the
, args = "your ad could be here" -- configuration values
}} {
, Bash2 = { module = "Process", params = -- Configure the addresses and ports bffh listens on
{ cmd = "./examples/actor.sh" listens = [
, args = "this is a different one" -- BFFH binds a port for every listen object in this array.
}} -- Each listen object is of the format { address = <STRING>, port = <INTEGER> }
, FailBash = { module = "Process", params = -- If you don't specify a port bffh will use the default of `59661`
{ cmd = "./examples/fail-actor.sh" -- 'address' can be a IP address or a hostname
}} -- If bffh can not bind a port for the specified combination if will log an error but *continue with the remaining ports*
} { address = "127.0.0.1", port = 59661 },
, init_connections = [] : List { machine : Text, initiator : Text } { address = "::1", port = 59661 },
--, init_connections = [{ machine = "Testmachine", initiator = "Initiator" }] { address = "192.168.0.114", port = 59661 }
, initiators = {=} ],
--{ Initiator = { module = "Dummy", params = { uid = "Testuser" } } }
, listens = -- Configure TLS. BFFH requires a PEM-encoded certificate and the associated key as two separate files
[ { address = "127.0.0.1", port = Some 59661 } certfile = "examples/self-signed-cert.pem",
, { address = "::1", port = Some 59661 } keyfile = "examples/self-signed-key.pem",
, { address = "192.168.0.114", port = Some 59661 }
] -- BFFH right now requires a running MQTT broker.
, machines = mqtt_url = "tcp://localhost:1883",
{ Testmachine =
{ description = "A test machine" -- Path to the database file for bffh. bffh will in fact create two files; ${db_path} and ${db_path}.lock.
, wiki = "test" -- BFFH will *not* create any directories so ensure that the directory exists and the user running bffh has write
, disclose = "lab.test.read" -- access into them.
, manage = "lab.test.admin" db_path = "/tmp/bffh",
, name = "MachineA"
, read = "lab.test.read" -- In dhall you can also easily import definitions from other files, e.g. you could write
, write = "lab.test.write" -- roles = ./roles.dhall
roles = {
-- Role definitions
-- A role definition is of the form
-- rolename = {
-- parents = [<list of role names to inherit from>],
-- permissions = [<list of perm rules>],
-- }
--
-- Role names are case sensitive, so RoleName != rolename.
--
-- If you want either parents or permissions to be empty its best to completely skip it:
testrole = {
permissions = [ "lab.some.admin" ]
},
somerole = {
parents = ["testparent"],
-- "Permissions" are formatted as Perm Rules, so you can use the wildcards '*' and '+'
permissions = [ "lab.test.*" ]
},
-- Roles can inherit from each other. In that case a member of e.g. 'somerole' that inherits from
-- 'testparent' will have all the permissions of 'somerole' AND 'testparent' assigned to them.
-- Right now permissions are stricly additive so you can't take a permission away in a child role that a parent
-- role grants.
testparent = {
permissions = [
"lab.some.write",
"lab.some.read",
"lab.some.disclose"
]
}
}, },
Another =
{ wiki = "test_another" -- Configure machines
, category = "test" -- "Machines" (which in future will be more appropiately named "resources") are the main thing bffh is concerned
, disclose = "lab.test.read" -- with.
, manage = "lab.test.admin" -- You can define an almost limitless amount of machines (well 2^64 - 1, so 18_446_744_073_709_551_615 to be precise)
, name = "Another" -- Each of these machines can then have several "actors" and "initiators" assigned
, read = "lab.test.read" machines = {
, write = "lab.test.write" Testmachine = {
-- A machine comes with two "names". The id above ("Testmachine") and the "name" ("MachineA").
-- The id is what you'll use in the config format and is strictly limited to alphanumeric characters and '_'
-- and must begin with a letter. Most importantly you CAN NOT use '-' or spaces in an identifier
-- (dhall makes this technically possible but you can break things in subtle ways)
-- REQUIRED. The "name" of a machine is what will be presented to humans. It can contain all unicode
-- including spaces and nonprintable characters.
-- A name SHOULD be short but unique.
name = "MachineA",
-- OPTIONAL. A description can be assigned to machines. It will also only be shown to humans. Thus it is
-- once again limited only to unicode. If you want to provide your users with important additional
-- information other than the name this is the place to do it.
description = "A test machine",
-- OPTIONAL. If you have a wiki going into more detail how to use a certain machine or what to keep in
-- mind when using it you can provide a URL here that will be presented to users.
wiki = "https://wiki.example.org/machineA",
-- OPTIONAL. You can assign categories to machines to allow clients to group/filter machines by them.
category = "Testcategory",
-- REQUIRED.
-- Each machine MUST have *all* Permission levels assigned to it.
-- Permissions aren't PermRules as used in the 'roles' definitions but must be precise without wildcards.
-- Permission levels aren't additive, so a user having 'manage' permission does not automatically get
-- 'read' or 'write' permission.
-- (Note, disclose is not fully implemented at the moment)
-- Users lacking 'disclose' will not be informed about this machine in any way and it will be hidden from
-- them in the client. Usually the best idea is to assign 'read' and 'disclose' to the same permission.
disclose = "lab.test.read",
-- Users lacking 'read' will be shown a machine including name, description, category and wiki but not
-- it's current state. The current user is not disclosed.
read = "lab.test.read",
-- The 'write' permission allows to 'use' the machine.
write = "lab.test.write",
-- Manage represents the 'superuser' permission. Users with this permission can force set any state and
-- read out the current user
manage = "lab.test.admin"
},
Another = {
wiki = "test_another",
category = "test",
disclose = "lab.test.read",
manage = "lab.test.admin",
name = "Another",
read = "lab.test.read",
write = "lab.test.write"
},
Yetmore = {
description = "Yet more test machines",
disclose = "lab.test.read",
manage = "lab.test.admin",
name = "Yetmore",
read = "lab.test.read",
write = "lab.test.write"
}
}, },
Yetmore =
{ description = "Yet more test machines" -- Actor configuration. Actors are how bffh affects change in the real world by e.g. switching a power socket
, disclose = "lab.test.read" -- using a shelly
, manage = "lab.test.admin" actors = {
, name = "Yetmore" -- Actors similarly to machines have an 'id'. This id (here "Shelly1234") is limited to Alphanumeric ASCII
, read = "lab.test.read" -- and must begin with a letter.
, write = "lab.test.write" Shelly1234 = {
} -- Actors are modular pieces of code that are loaded as required. The "Shelly" module will send
} -- activation signals to a shelly switched power socket over MQTT
, mqtt_url = "tcp://localhost:1883" module = "Shelly",
, db_path = "/tmp/bffh" -- Actors can have arbitrary parameters passed to them, varying by actor module.
, roles = params = {
{ testrole = -- For Shelly you can configure the MQTT topic segment it uses. Shellies listen to a specific topic
{ permissions = [ "lab.test.*" ] } -- containing their name (which is usually of the form "shelly_<id>" but can be changed).
, somerole = -- If you do not configure a topic here the actor will use it's 'id' (in this case "Shelly1234").
{ parents = ["testparent"] topic = "Topic1234"
, permissions = [ "lab.some.admin" ] }
} },
, testparent =
{ permissions = Bash = {
[ "lab.some.write" -- The "Process" module runs a given script or command on state change.
, "lab.some.read" -- bffh invoces the given cmd as `$ ${cmd} ${args} ${id} ${state}` so e.g. as
, "lab.some.disclose" -- `$ ./examples/actor.sh your ad could be here Bash inuse`
] module = "Process",
} params = {
} -- which is configured by the (required) 'cmd' parameter. Paths are relative to PWD of bffh. Systemd
, certfile = "examples/self-signed-cert.pem" -- and similar process managers may change this PWD so it's usually the most future-proof to use
, keyfile = "examples/self-signed-key.pem" -- absolute paths.
} cmd = "./examples/actor.sh",
-- You can pass static args in here, these will be passed to every invocation of the command by this actor.
-- args passed here are split by whitespace, so these here will be passed as 5 separate arguments
args = "your ad could be here"
}
},
Bash2 = { module = "Process", params = { cmd = "./examples/actor.sh" , args = "this is a different one" }},
FailBash = { module = "Process", params = { cmd = "./examples/fail-actor.sh" }}
},
-- Linkng up machines to actors
-- Actors need to be connected to machines to be useful. A machine can be connected to multiple actors, but one
-- actor can only be connected to one machine.
actor_connections = [
{ machine = "Testmachine", actor = "Shelly1234" },
{ machine = "Another", actor = "Bash" },
{ machine = "Yetmore", actor = "Bash2" },
{ machine = "Yetmore", actor = "FailBash"}
],
-- Initiators are configured almost the same way as Actors, refer to actor documentation for more details
-- The below '{=}' is what you need if you want to define *no* initiators at all and only use the API with apps
-- to let people use machines.
initiators = {=},
-- The "Dummy" initiator will try to use and return a machine as the given user every few seconds. It's good to
-- test your system but will spam your log so is disabled by default.
--{ Initiator = { module = "Dummy", params = { uid = "Testuser" } } }
-- Linking up machines to initiators. Similar to actors a machine can have several initiators assigned but an
-- initiator can only be assigned to one machine.
-- The below is once again how you have to define *no* initiators.
init_connections = [] : List { machine : Text, initiator : Text }
-- init_connections = [{ machine = "Testmachine", initiator = "Initiator" }]
}

View File

@ -1,19 +0,0 @@
[anotherrole]
[testrole]
permissions = [
"lab.test.*"
]
[somerole]
parents = ["testparent/lmdb"]
permissions = [
"lab.some.admin"
]
[testparent]
permissions = [
"lab.some.write",
"lab.some.read",
"lab.some.disclose",
]

View File

@ -1,13 +1,13 @@
[Testuser] [Testuser]
# Define them in roles.toml as well # These roles have to be defined in 'bffh.dhall'.
# Non-existant roles will not crash the server but print a `WARN` level message in the
# server log in the form "Did not find role somerole/internal while trying to tally".
roles = ["somerole/internal", "testrole/internal"] roles = ["somerole/internal", "testrole/internal"]
# If two or more users want to use the same machine at once the higher prio # The password will be hashed using argon2id on load time and is not available in plaintext afterwards.
# wins
priority = 0
passwd = "secret" passwd = "secret"
# You can add whatever random data you want. # You can add whatever random data you want.
# It will get stored in the `kv` field in UserData. # It will get stored in the `kv` field in UserData.
# This is not used for anything at the moment
noot = "noot!" noot = "noot!"

View File

@ -200,8 +200,7 @@ async fn fill_machine_builder(
builder.set_urn(&format!("urn:fabaccess:resource:{}", id.as_ref())); builder.set_urn(&format!("urn:fabaccess:resource:{}", id.as_ref()));
let machineapi = Machine::new(user.clone(), perms, machine.clone()); let machineapi = Machine::new(user.clone(), perms, machine.clone());
let state = machine.get_status().await; if perms.write {
if perms.write && state == Status::Free {
builder.set_use(capnp_rpc::new_client(machineapi.clone())); builder.set_use(capnp_rpc::new_client(machineapi.clone()));
} }
if perms.manage { if perms.manage {
@ -229,7 +228,9 @@ async fn fill_machine_builder(
Status::Reserved(_) => MachineState::Reserved, Status::Reserved(_) => MachineState::Reserved,
Status::ToCheck(_) => MachineState::ToCheck, Status::ToCheck(_) => MachineState::ToCheck,
}; };
builder.set_state(s); if perms.read {
builder.set_state(s);
}
builder.set_info(capnp_rpc::new_client(machineapi)); builder.set_info(capnp_rpc::new_client(machineapi));
} }

View File

@ -58,6 +58,14 @@ pub struct Config {
pub keyfile: PathBuf, pub keyfile: PathBuf,
} }
pub(crate) fn deser_option<'de, D, T>(d: D) -> std::result::Result<Option<T>, D::Error>
where D: serde::Deserializer<'de>, T: serde::Deserialize<'de>,
{
Ok(T::deserialize(d).ok())
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoleConfig { pub struct RoleConfig {
#[serde(default = "Vec::new")] #[serde(default = "Vec::new")]
@ -69,6 +77,8 @@ pub struct RoleConfig {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Listen { pub struct Listen {
pub address: String, pub address: String,
#[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "deser_option")]
pub port: Option<u16>, pub port: Option<u16>,
} }

View File

@ -32,6 +32,8 @@ use crate::Error::Denied;
use crate::network::MachineMap; use crate::network::MachineMap;
use crate::space; use crate::space;
use crate::config::deser_option;
pub struct Machines { pub struct Machines {
machines: Vec<Machine> machines: Vec<Machine>
} }
@ -376,12 +378,6 @@ pub struct MachineDescription {
pub privs: access::PrivilegesBuf, pub privs: access::PrivilegesBuf,
} }
fn deser_option<'de, D, T>(d: D) -> std::result::Result<Option<T>, D::Error>
where D: serde::Deserializer<'de>, T: serde::Deserialize<'de>,
{
Ok(T::deserialize(d).ok())
}
impl MachineDescription { impl MachineDescription {
pub fn load_file<P: AsRef<Path>>(path: P) -> Result<HashMap<MachineIdentifier, MachineDescription>> { pub fn load_file<P: AsRef<Path>>(path: P) -> Result<HashMap<MachineIdentifier, MachineDescription>> {
let content = fs::read(path)?; let content = fs::read(path)?;