From 0da32133958525619904f16392dd1c804bcab416 Mon Sep 17 00:00:00 2001 From: Nadja Reitzenstein Date: Thu, 6 Jan 2022 18:19:02 +0100 Subject: [PATCH] Improve documentation around example setup Fixes: #38 --- examples/bffh.dhall | 282 +++++++++++++++++++++++++++++++------------- examples/roles.toml | 19 --- examples/users.toml | 10 +- src/api/machines.rs | 7 +- src/config.rs | 10 ++ src/machine.rs | 8 +- 6 files changed, 224 insertions(+), 112 deletions(-) delete mode 100644 examples/roles.toml diff --git a/examples/bffh.dhall b/examples/bffh.dhall index a541710..2a7ae5d 100644 --- a/examples/bffh.dhall +++ b/examples/bffh.dhall @@ -1,81 +1,205 @@ --- { actor_connections = [] : List { _1 : Text, _2 : Text } -{ actor_connections = - -- Link up machines to actors - [ { machine = "Testmachine", actor = "Shelly1234" } - , { machine = "Another", actor = "Bash" } - -- One machine can have as many actors as it wants - , { machine = "Yetmore", actor = "Bash2" } - , { machine = "Yetmore", actor = "FailBash"} - ] -, actors = - { Shelly1234 = { module = "Shelly", params = - { topic = "Topic1234" }} - , Bash = { module = "Process", params = - { cmd = "./examples/actor.sh" - , 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" - }} - } - , init_connections = [] : List { machine : Text, initiator : Text } - --, init_connections = [{ machine = "Testmachine", initiator = "Initiator" }] - , initiators = {=} - --{ Initiator = { module = "Dummy", params = { uid = "Testuser" } } } -, listens = - [ { address = "127.0.0.1", port = Some 59661 } - , { address = "::1", port = Some 59661 } - , { address = "192.168.0.114", port = Some 59661 } - ] -, machines = - { Testmachine = - { description = "A test machine" - , wiki = "test" - , disclose = "lab.test.read" - , manage = "lab.test.admin" - , name = "MachineA" - , read = "lab.test.read" - , write = "lab.test.write" +{- Main configuration file for bffh + - ================================ + - + - In this configuration file you configure almost all parts of how bffh operates, but most importantly: + - * Machines + - * Initiators and Actors + - * Which Initiators and Actors relate to which machine(s) + - * Roles and the permissions granted by them + -} + +-- The config is in the configuration format/language dhall. You can find more information about dhall over at +-- https://dhall-lang.org + +-- (Our) Dhall is somewhat similar to JSON and YAML in that it expects a top-level object containing the +-- configuration values +{ + -- Configure the addresses and ports bffh listens on + listens = [ + -- BFFH binds a port for every listen object in this array. + -- Each listen object is of the format { address = , port = } + -- If you don't specify a port bffh will use the default of `59661` + -- '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 }, + { address = "::1", port = 59661 }, + { address = "192.168.0.114", port = 59661 } + ], + + -- Configure TLS. BFFH requires a PEM-encoded certificate and the associated key as two separate files + certfile = "examples/self-signed-cert.pem", + keyfile = "examples/self-signed-key.pem", + + -- BFFH right now requires a running MQTT broker. + mqtt_url = "tcp://localhost:1883", + + -- Path to the database file for bffh. bffh will in fact create two files; ${db_path} and ${db_path}.lock. + -- BFFH will *not* create any directories so ensure that the directory exists and the user running bffh has write + -- access into them. + db_path = "/tmp/bffh", + + -- In dhall you can also easily import definitions from other files, e.g. you could write + -- roles = ./roles.dhall + roles = { + -- Role definitions + -- A role definition is of the form + -- rolename = { + -- parents = [], + -- permissions = [], + -- } + -- + -- 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" - , category = "test" - , disclose = "lab.test.read" - , manage = "lab.test.admin" - , name = "Another" - , read = "lab.test.read" - , write = "lab.test.write" + + -- Configure machines + -- "Machines" (which in future will be more appropiately named "resources") are the main thing bffh is concerned + -- with. + -- You can define an almost limitless amount of machines (well 2^64 - 1, so 18_446_744_073_709_551_615 to be precise) + -- Each of these machines can then have several "actors" and "initiators" assigned + machines = { + 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" - , disclose = "lab.test.read" - , manage = "lab.test.admin" - , name = "Yetmore" - , read = "lab.test.read" - , write = "lab.test.write" - } - } -, mqtt_url = "tcp://localhost:1883" -, db_path = "/tmp/bffh" -, roles = - { testrole = - { permissions = [ "lab.test.*" ] } - , somerole = - { parents = ["testparent"] - , permissions = [ "lab.some.admin" ] - } - , testparent = - { permissions = - [ "lab.some.write" - , "lab.some.read" - , "lab.some.disclose" - ] - } - } -, certfile = "examples/self-signed-cert.pem" -, keyfile = "examples/self-signed-key.pem" -} + + -- Actor configuration. Actors are how bffh affects change in the real world by e.g. switching a power socket + -- using a shelly + actors = { + -- Actors similarly to machines have an 'id'. This id (here "Shelly1234") is limited to Alphanumeric ASCII + -- and must begin with a letter. + 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 + module = "Shelly", + -- Actors can have arbitrary parameters passed to them, varying by actor module. + params = { + -- For Shelly you can configure the MQTT topic segment it uses. Shellies listen to a specific topic + -- containing their name (which is usually of the form "shelly_" but can be changed). + -- If you do not configure a topic here the actor will use it's 'id' (in this case "Shelly1234"). + topic = "Topic1234" + } + }, + + Bash = { + -- The "Process" module runs a given script or command on state change. + -- bffh invoces the given cmd as `$ ${cmd} ${args} ${id} ${state}` so e.g. as + -- `$ ./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 + -- and similar process managers may change this PWD so it's usually the most future-proof to use + -- 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" }] +} \ No newline at end of file diff --git a/examples/roles.toml b/examples/roles.toml deleted file mode 100644 index 2c91a30..0000000 --- a/examples/roles.toml +++ /dev/null @@ -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", -] diff --git a/examples/users.toml b/examples/users.toml index 3c4aa37..719f2fb 100644 --- a/examples/users.toml +++ b/examples/users.toml @@ -1,13 +1,13 @@ [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"] -# If two or more users want to use the same machine at once the higher prio -# wins -priority = 0 - +# The password will be hashed using argon2id on load time and is not available in plaintext afterwards. passwd = "secret" # You can add whatever random data you want. # It will get stored in the `kv` field in UserData. +# This is not used for anything at the moment noot = "noot!" diff --git a/src/api/machines.rs b/src/api/machines.rs index cc2d5c5..f754d52 100644 --- a/src/api/machines.rs +++ b/src/api/machines.rs @@ -200,8 +200,7 @@ async fn fill_machine_builder( builder.set_urn(&format!("urn:fabaccess:resource:{}", id.as_ref())); let machineapi = Machine::new(user.clone(), perms, machine.clone()); - let state = machine.get_status().await; - if perms.write && state == Status::Free { + if perms.write { builder.set_use(capnp_rpc::new_client(machineapi.clone())); } if perms.manage { @@ -229,7 +228,9 @@ async fn fill_machine_builder( Status::Reserved(_) => MachineState::Reserved, Status::ToCheck(_) => MachineState::ToCheck, }; - builder.set_state(s); + if perms.read { + builder.set_state(s); + } builder.set_info(capnp_rpc::new_client(machineapi)); } \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 974fe54..53e0a78 100644 --- a/src/config.rs +++ b/src/config.rs @@ -58,6 +58,14 @@ pub struct Config { pub keyfile: PathBuf, } + +pub(crate) fn deser_option<'de, D, T>(d: D) -> std::result::Result, D::Error> + where D: serde::Deserializer<'de>, T: serde::Deserialize<'de>, +{ + Ok(T::deserialize(d).ok()) +} + + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RoleConfig { #[serde(default = "Vec::new")] @@ -69,6 +77,8 @@ pub struct RoleConfig { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Listen { pub address: String, + + #[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "deser_option")] pub port: Option, } diff --git a/src/machine.rs b/src/machine.rs index 0ff2268..2308303 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -32,6 +32,8 @@ use crate::Error::Denied; use crate::network::MachineMap; use crate::space; +use crate::config::deser_option; + pub struct Machines { machines: Vec } @@ -376,12 +378,6 @@ pub struct MachineDescription { pub privs: access::PrivilegesBuf, } -fn deser_option<'de, D, T>(d: D) -> std::result::Result, D::Error> - where D: serde::Deserializer<'de>, T: serde::Deserialize<'de>, -{ - Ok(T::deserialize(d).ok()) -} - impl MachineDescription { pub fn load_file>(path: P) -> Result> { let content = fs::read(path)?;