Compare commits

...

49 Commits

Author SHA1 Message Date
314145084e update schema to latest 2025-03-05 23:51:27 +01:00
5e0d615ada make schema (API) submodule static to allow easy project-forking 2025-03-05 23:09:37 +01:00
e4b7aa8766 remove dirty examples/ dir. proper and up to date examples can be found at https://gitlab.com/fabinfra/fabaccess/demos-environments for instance 2025-03-04 00:35:25 +01:00
bbf0e77571 remove obsolete test/ directory 2025-02-24 13:17:33 +01:00
40ca2acdb7 cleanup/update contributing infos 2025-02-20 16:44:37 +01:00
3e29df8b2c Remove obsolete installation docs, href the user to docs.fab-access.org instead 2025-02-20 16:18:42 +01:00
fdde8a933c Cleanup: remove /docs dir because we put it to https://docs.fab-access.org 2025-02-20 15:35:19 +01:00
Jonathan Krebs
c8f0337c1e set release date for 0.4.3 2025-02-11 16:56:25 +01:00
Jonathan Krebs
151c04d9df record changes since 0.4.2 2025-01-24 17:39:52 +01:00
Jonathan Krebs
1d2ba9eddc implement dumping and loading the full database 2025-01-24 04:45:03 +01:00
Jonathan Krebs
ee21f74d2d minimal dependency update to make it compatible with current rust 2025-01-24 04:30:37 +01:00
=
06083a63e3 Fix compilation error caused by overlooked renames: diflouroborane -> difluoroborane 2024-12-15 22:29:14 +01:00
57a0436ca1 fix typo; fixes https://gitlab.com/fabinfra/fabaccess/bffh/-/issues/68 2024-12-14 21:54:01 +01:00
Jonathan Krebs
8b15acf983 remove warnings around initiator loading. cleaner error handling remains todo. 2024-12-13 15:32:21 +01:00
Jonathan Krebs
40ba114e61 fix warnings: at the moment configuration by environment variables is not implemented 2024-12-13 15:32:21 +01:00
Jonathan Krebs
c2c34ede67 fix warnings: remove unused muts and variables 2024-12-13 15:32:21 +01:00
Jonathan Krebs
2b0fe0e868 add some error handling, mostly to quiet warnings 2024-12-13 15:32:21 +01:00
Jonathan Krebs
fbfb76c34e fix warnings: some more easy cases 2024-12-13 15:32:21 +01:00
Jonathan Krebs
971dee36fd fix warnings: replace some mem::replace with assignments 2024-12-13 15:32:21 +01:00
Jonathan Krebs
41983e6039 remove unused imports from bffhd 2024-12-13 15:32:21 +01:00
Falko Richter
ca25cd83d0 Update INSTALL.md 2024-11-20 12:29:45 +00:00
Jonathan Krebs
9805f4ee04 lightproc: drop span guard before deallocating the process
This should fix #77
2024-11-09 11:44:43 +01:00
Falko Richter
66877159f0 fixed links to the other repos 2024-07-22 06:53:45 +00:00
Nadja Reitzenstein
a9143b0cdd pin toolchain to a known good version while we get that 'fun' segfault 2023-02-23 17:00:19 +01:00
Nadja Reitzenstein
165b269d4f Fix example config 2023-02-23 14:25:44 +01:00
Nadja Reitzenstein
e35e2b7334 cargo fmt 2023-02-09 17:07:31 +01:00
Nadja Reitzenstein
98c2e3fd01 rsasl update 2023-02-09 17:07:22 +01:00
Kai Jan Kriegel
27f5413e3d i guess i forgot how to format string? 2023-02-06 18:07:42 +01:00
Kai Jan Kriegel
55e9bf6e2b check the configured space urn and not a hardcoded one 2023-02-03 21:35:53 +01:00
Kai Jan Kriegel
4cdbfd8925 fix bind / unbind card 2023-02-03 08:20:39 +01:00
Nadja Reitzenstein
7a85667a44 Whoops that was a premature push 2023-01-31 16:18:18 +01:00
Nadja Reitzenstein
cf3853263a Make spacename/instanceurl required and enable card interface 2023-01-31 16:16:00 +01:00
Nadja Reitzenstein
946a08c19c I should really read my own documentation sometimes. 2023-01-11 14:51:38 +01:00
Nadja Reitzenstein
e42a32934a Implement remaining card management 2023-01-09 17:05:48 +01:00
Nadja Reitzenstein
24c02fccff Implement partial card mgmnt 2023-01-09 17:05:48 +01:00
Nadja Reitzenstein
beecb54d38 Move miette towards edges of BFFH for more structured error reporting 2023-01-09 17:05:46 +01:00
Kai Kriegel
0716a75ee6 Add support for binary FabReader Mechanism 2023-01-02 05:00:29 +00:00
Kai Kriegel
0380e02f3f reworked CI 2023-01-02 03:59:09 +00:00
Nadja Reitzenstein
0d2cd6f376 No path deps for good reasons™ 2022-11-02 15:01:44 +01:00
TheJoKlLa
410ed8cb33 Revert binarayfabfire 2022-11-01 15:00:52 +01:00
Kai Jan Kriegel
7a941c3338 actually return the value 2022-11-01 13:24:04 +00:00
Kai Jan Kriegel
8776fa3ca2 return correct length from step 2022-11-01 13:24:04 +00:00
Kai Jan Kriegel
5c4cb32d1a initial support for binary version of FabFire 2022-11-01 13:24:04 +00:00
Nadja Reitzenstein
1971515601 Merge branch 'feature/rsasl-update' into development
* feature/rsasl-update:
  Update to latest rsasl
  Port rsasl
2022-11-01 10:48:14 +01:00
Nadja Reitzenstein
0ed53f5cc9 Update to latest rsasl 2022-11-01 10:47:51 +01:00
Nadja Reitzenstein
47524ef038 Merge remote-tracking branch 'origin/development' into development
* origin/development:
  Added paho-mqtt
2022-10-07 13:43:50 +02:00
Nadja Reitzenstein
a8cc1be87d Merge tag 'v0.4.2' into development
v0.4.2

* tag 'v0.4.2':
2022-10-07 13:43:12 +02:00
Nadja Reitzenstein
3cf152a164 Port rsasl 2022-10-05 17:28:47 +02:00
TheJoKlLa
29bfe61a2c Added paho-mqtt 2022-08-24 12:42:28 +00:00
83 changed files with 2432 additions and 2115 deletions

View File

@ -2,21 +2,28 @@
# Additionally, lint the code before anything else to fail more quickly # Additionally, lint the code before anything else to fail more quickly
stages: stages:
- lint - lint
- check
- build - build
- test - test
- release - release
- dockerify - dockerify
default: default:
image: "rust:latest" image: "registry.gitlab.com/fabinfra/rust-builder:latest"
tags: tags:
- linux - linux
- docker - docker
- fabinfra
variables: variables:
GIT_SUBMODULE_STRATEGY: recursive GIT_SUBMODULE_STRATEGY: recursive
CARGO_HOME: $CI_PROJECT_DIR/cargo CARGO_HOME: $CI_PROJECT_DIR/cargo
APT_CACHE_DIR: $CI_PROJECT_DIR/apt APT_CACHE_DIR: $CI_PROJECT_DIR/apt
FF_USE_FASTZIP: "true" # enable fastzip - a faster zip implementation that also supports level configuration.
ARTIFACT_COMPRESSION_LEVEL: fast # can also be set to fastest, fast, slow and slowest. If just enabling fastzip is not enough try setting this to fastest or fast.
CACHE_COMPRESSION_LEVEL: fastest # same as above, but for caches
TRANSFER_METER_FREQUENCY: 5s # will display transfer progress every 5 seconds for artifacts and remote caches.
# cache dependencies and build environment to speed up setup # cache dependencies and build environment to speed up setup
cache: cache:
@ -26,10 +33,6 @@ cache:
- cargo/ - cargo/
- target/ - target/
# install build dependencies
before_script:
- apt-get update -yqq
- apt-get install -o dir::cache::archives="$APT_CACHE_DIR" -yqq --no-install-recommends capnproto build-essential cmake clang libclang-dev jq
.lints: .lints:
stage: lint stage: lint
@ -41,7 +44,6 @@ before_script:
lint:clippy: lint:clippy:
extends: .lints extends: .lints
script: script:
- rustup component add clippy
- cargo clippy -V - cargo clippy -V
- echo -e "\e[0Ksection_start:`date +%s`:clippy_output\r\e[0Kcargo clippy output" - echo -e "\e[0Ksection_start:`date +%s`:clippy_output\r\e[0Kcargo clippy output"
- cargo clippy -- --no-deps - cargo clippy -- --no-deps
@ -51,15 +53,14 @@ lint:clippy:
lint:fmt: lint:fmt:
extends: .lints extends: .lints
script: script:
- rustup component add rustfmt
- cargo fmt --version - cargo fmt --version
- echo -e "\e[0Ksection_start:`date +%s`:rustfmt_output\r\e[0KChanges suggested by rustfmt" - echo -e "\e[0Ksection_start:`date +%s`:rustfmt_output\r\e[0KChanges suggested by rustfmt"
- cargo fmt --check -- -v - cargo fmt --check -- -v
- echo -e "\e[0Ksection_end:`date +%s`:rustfmt_output\r\e[0K" - echo -e "\e[0Ksection_end:`date +%s`:rustfmt_output\r\e[0K"
# Check if the code builds on rust stable # Check if the code builds on rust stable
stable:build: stable:check:
stage: build stage: check
only: only:
- main - main
- development - development
@ -70,9 +71,94 @@ stable:build:
- cargo check --verbose - cargo check --verbose
- echo -e "\e[0Ksection_end:`date +%s`:build_output\r\e[0K" - echo -e "\e[0Ksection_end:`date +%s`:build_output\r\e[0K"
# Check if the code builds on rust stable on armv7
stable:check:armhf:
stage: check
only:
- main
- development
- merge_requests
before_script:
- mkdir -p $CARGO_HOME
- cp cargo-cross-config $CARGO_HOME/config.toml
script:
- rustc +stable --version && cargo --version
- echo -e "\e[0Ksection_start:`date +%s`:build_output\r\e[0KOutput of cargo check with target armv7-unknown-linux-gnueabihf"
- cargo check --verbose --target armv7-unknown-linux-gnueabihf
- echo -e "\e[0Ksection_end:`date +%s`:build_output\r\e[0K"
# Check if the code builds on rust stable on arm64
stable:check:arm64:
stage: check
only:
- main
- development
- merge_requests
before_script:
- mkdir -p $CARGO_HOME
- cp cargo-cross-config $CARGO_HOME/config.toml
script:
- rustc +stable --version && cargo --version
- echo -e "\e[0Ksection_start:`date +%s`:build_output\r\e[0KOutput of cargo check with target aarch64-unknown-linux-gnu"
- cargo check --verbose --target aarch64-unknown-linux-gnu
- echo -e "\e[0Ksection_end:`date +%s`:build_output\r\e[0K"
# Check if the code builds on rust stable
stable:build:amd64:
stage: build
only:
- main
- development
- merge_requests
script:
- rustc +stable --version && cargo --version
- echo -e "\e[0Ksection_start:`date +%s`:build_output\r\e[0KOutput of cargo build with target x86_64-unknown-linux-gnu"
- cargo build --release --target x86_64-unknown-linux-gnu
- echo -e "\e[0Ksection_end:`date +%s`:build_output\r\e[0K"
artifacts:
paths:
- target/x86_64-unknown-linux-gnu/release/bffhd
# Check if the code builds on rust stable on armv7
stable:build:armhf:
stage: build
only:
- main
- development
before_script:
- mkdir -p $CARGO_HOME
- cp cargo-cross-config $CARGO_HOME/config.toml
script:
- rustc +stable --version && cargo --version
- echo -e "\e[0Ksection_start:`date +%s`:build_output\r\e[0KOutput of cargo build with target armv7-unknown-linux-gnueabihf"
- cargo build --release --target armv7-unknown-linux-gnueabihf
- echo -e "\e[0Ksection_end:`date +%s`:build_output\r\e[0K"
artifacts:
paths:
- target/armv7-unknown-linux-gnueabihf/release/bffhd
# Check if the code builds on rust stable on arm64
stable:build:arm64:
stage: build
only:
- main
- development
before_script:
- mkdir -p $CARGO_HOME
- cp cargo-cross-config $CARGO_HOME/config.toml
script:
- rustc +stable --version && cargo --version
- echo -e "\e[0Ksection_start:`date +%s`:build_output\r\e[0KOutput of cargo build with target aarch64-unknown-linux-gnu"
- cargo build --release --target aarch64-unknown-linux-gnu
- echo -e "\e[0Ksection_end:`date +%s`:build_output\r\e[0K"
artifacts:
paths:
- target/aarch64-unknown-linux-gnu/release/bffhd
stable:test: stable:test:
stage: build stage: build
needs: ["stable:build"] needs: ["stable:check"]
only: only:
- main - main
- development - development
@ -80,14 +166,12 @@ stable:test:
script: script:
- echo -e "\e[0Ksection_start:`date +%s`:build_output\r\e[0KOutput of cargo test --no-run" - echo -e "\e[0Ksection_start:`date +%s`:build_output\r\e[0KOutput of cargo test --no-run"
- cargo test --verbose --no-run --workspace - cargo test --verbose --no-run --workspace
- echo -e "\e[0Ksection_end:`date +%s`:build_output\r\e[0K"
- cargo install --root $CARGO_HOME cargo2junit
.tests: .tests:
stage: test stage: test
needs: ["stable:test"] needs: ["stable:test"]
script: script:
- cargo test --workspace $TEST_TARGET -- -Z unstable-options --format json --report-time | $CARGO_HOME/bin/cargo2junit > report.xml - cargo test --workspace $TEST_TARGET -- -Z unstable-options --format json --report-time | cargo2junit > report.xml
artifacts: artifacts:
when: always when: always
reports: reports:
@ -114,6 +198,23 @@ unit test 3:3:
TEST_TARGET: "--examples" TEST_TARGET: "--examples"
extends: .tests extends: .tests
upload_binaries:
stage: release
image: curlimages/curl:latest
before_script: []
cache: []
dependencies:
- stable:build:amd64
- stable:build:armhf
- stable:build:arm64
script:
- 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file target/aarch64-unknown-linux-gnu/release/bffhd "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/bffhd/${CI_COMMIT_TAG}/bffhd_${VERSION}_linux_arm64"'
- 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file target/x86_64-unknown-linux-gnu/release/bffhd "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/bffhd/${CI_COMMIT_TAG}/bffhd_${VERSION}_linux_amd64"'
- 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file target/armv7-unknown-linux-gnueabihf/release/bffhd "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/bffhd/${VERSION}/bffhd_${VERSION}_linux_arm"'
rules:
- if: $CI_COMMIT_TAG =~ "release/.*"
when: never
- if: $CI_COMMIT_BRANCH == "main"
release_prepare: release_prepare:
stage: release stage: release
@ -144,32 +245,106 @@ release_job:
name: "BFFH $VERSION" name: "BFFH $VERSION"
description: "GitLab CI auto-created release" description: "GitLab CI auto-created release"
tag_name: "release/$VERSION" tag_name: "release/$VERSION"
assets:
links:
- name: 'bffhd AMD64'
url: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/bffhd/${VERSION}/bffhd_${VERSION}_linux_amd64"
- name: 'bffhd ARMv7'
url: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/bffhd/${VERSION}/bffhd_${VERSION}_linux_arm"
- name: 'bffhd ARM64'
url: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/bffhd/${VERSION}/bffhd_${VERSION}_linux_arm64"
build:docker-releases: build:docker-releases:
stage: dockerify stage: dockerify
image: image: jdrouet/docker-with-buildx:latest
name: gcr.io/kaniko-project/executor:v1.6.0-debug dependencies:
entrypoint: [""] - stable:build:amd64
- stable:build:armhf
- stable:build:arm64
tags:
- linux
- docker
- fabinfra
variables:
DOCKER_HOST: tcp://docker:2375/
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
TRIVY_NO_PROGRESS: "true"
TRIVY_CACHE_DIR: ".trivycache/"
services:
- docker:dind
before_script: before_script:
- '' - export TRIVY_VERSION=$(wget -qO - "https://api.github.com/repos/aquasecurity/trivy/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
- echo $TRIVY_VERSION
- wget --no-verbose https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz -O - | tar -zxvf -
script: script:
- mkdir -p /kaniko/.docker - docker login $CI_REGISTRY -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD"
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json - docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
- /kaniko/executor --force --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG - docker buildx create --name cibuilder --driver docker-container --use
- docker buildx ls
- docker buildx inspect --bootstrap
- docker buildx build --platform linux/arm/v7,linux/arm64,linux/amd64 -t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG .
- docker buildx build --load --platform linux/amd64 -t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG .
# Build report
- ./trivy image --exit-code 0 --format template --template "@contrib/gitlab.tpl" -o gl-container-scanning-report.json $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
# Print report
- ./trivy image --exit-code 0 --severity HIGH $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
# Fail on severe vulnerabilities
- ./trivy image --exit-code 1 --severity CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
cache:
paths:
- .trivycache/
artifacts:
reports:
container_scanning: gl-container-scanning-report.json
rules: rules:
- if: $CI_COMMIT_TAG =~ "release/.*" - if: $CI_COMMIT_TAG =~ "release/.*"
when: never when: never
build:docker-development: build:docker-development:
stage: dockerify stage: dockerify
image: image: jdrouet/docker-with-buildx:latest
name: gcr.io/kaniko-project/executor:v1.6.0-debug dependencies:
entrypoint: [""] - stable:build:amd64
- stable:build:armhf
- stable:build:arm64
tags:
- linux
- docker
- fabinfra
variables:
DOCKER_HOST: tcp://docker:2375/
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
TRIVY_NO_PROGRESS: "true"
TRIVY_CACHE_DIR: ".trivycache/"
services:
- docker:dind
before_script: before_script:
- '' - export TRIVY_VERSION=$(wget -qO - "https://api.github.com/repos/aquasecurity/trivy/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
- echo $TRIVY_VERSION
- wget --no-verbose https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz -O - | tar -zxvf -
script: script:
- mkdir -p /kaniko/.docker - docker login $CI_REGISTRY -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD"
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json - docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
- /kaniko/executor --force --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:dev-latest - docker buildx create --name cibuilder --driver docker-container --use
- docker buildx ls
- docker buildx inspect --bootstrap
- docker buildx build --platform linux/arm/v7,linux/arm64,linux/amd64 -t $CI_REGISTRY_IMAGE:development .
- docker buildx build --load --platform linux/amd64 -t $CI_REGISTRY_IMAGE:development .
# Build report
- ./trivy image --exit-code 0 --format template --template "@contrib/gitlab.tpl" -o gl-container-scanning-report.json $CI_REGISTRY_IMAGE:development
# Print report
- ./trivy image --exit-code 0 --severity HIGH $CI_REGISTRY_IMAGE:development
# Fail on severe vulnerabilities
- ./trivy image --exit-code 1 --severity CRITICAL $CI_REGISTRY_IMAGE:development
- docker push $CI_REGISTRY_IMAGE:development
cache:
paths:
- .trivycache/
artifacts:
reports:
container_scanning: gl-container-scanning-report.json
only: only:
- development - development

2
.gitmodules vendored
View File

@ -1,4 +1,4 @@
[submodule "schema"] [submodule "schema"]
path = api/schema path = api/schema
url = ../fabaccess-api url = https://gitlab.com/fabinfra/fabaccess/fabaccess-api
branch = main branch = main

View File

@ -1,5 +1,17 @@
# Revision history for Difluoroborane # Revision history for Difluoroborane
## 0.4.3 -- 2025-02-11
* Adds binary version of FabFire authenitcation protocol
* Adds commands to dump and restore the full database as a TOML text file (`--dump-db` and `--load-db`)
* allows compilation with current stable Rust (1.84)
- Attention: The database format still relies on Rust data layout, so when updating the compiler, the database must be transfered as TOML dump.
Therefore, the `rust-toolchain.toml` file pinning `rustc` to version `1.66` is still in place.
* resolves a crash (use after free) when disconnecting a client.
* resolves some compiler warnings
## 0.4.2 -- TODO
## 0.4.1 -- 2022-04-24 ## 0.4.1 -- 2022-04-24
* Initial full implementation of the FabAccess 0.3 API, "Spigots of Berlin". * Initial full implementation of the FabAccess 0.3 API, "Spigots of Berlin".

View File

@ -2,120 +2,38 @@
Thank you for your interest in helping out the FabAccess system! Thank you for your interest in helping out the FabAccess system!
You found a bug, an exploit or a feature that doesn't work like it's documented? Please tell us You found a bug, an exploit or a feature that doesn't work like it's documented? Please tell us about it, see [Issues](#issues)
about it, see [Issues](#issues)
You have a feature request? Great, check out the paragraph on [Feature Requests](#feature-requests) You have a feature request? Great, check out the paragraph on [Feature Requests](#feature-requests)
## Issues ## Issues
While we try to not have any bugs or exploits or documentation bugs we're not perfect either. Thanks While we try to not have any bugs or exploits or documentation bugs we're not perfect either. Thanks for helping us out!
for helping us out!
We have labels that help us sort issues better, so if you know what would be the correct ones, We have labels that help us sort issues better, so if you know what would be the correct ones, please tag your issue with one or multiple keywords. See [Labels](https://gitlab.com/fabinfra/fabaccess/bffh/-/labels) to get an overview of all keywords and their use case.
please tag your issue:
- `documentation` if it's an documentation issue, be it lacking docs or even worse wrong docs.
- `bug` is for software bugs, unexpected behaviour, crashes and so on.
- `exploit` for any bugs that may be used as RCE, to escalate priviledges or some-such.
Don't worry if you aren't sure about the correct labels, an issue opened with no labels is much
better than no knowing about the issue!
Especially for bugs and exploits, please mark your issue as "confidential" if you think it impacts Especially for **bugs** and **exploits**, please mark your issue as "confidential" if you think it impacts the `stable` branch. If you're not sure, mark it as confidential anyway. It's easier to publish information than it is to un-publish information. You may also contact as by [mail](https://fab-access.org/impressum).
the `stable` branch. If you're not sure, mark it as confidential anyway. It's easier to publish
information than it is to un-publish information.
If you found an exploit and it's high-impact enough that you do not want to open an issue but
instead want direct contact with the developers, you can find public keys respectively fingerprints
for GPG, XMPP+OMEMO and Matrix+MegOlm in the git repository as blobs with tags assigned to them.
You can import the gpg key for dequbed either from the repository like so:
```
$ git cat-file -p keys/dequbed/gpg | gpg --import-key
```
Or from your local trusted gpg keyserver, and/or verify it using [keybase](https://keybase.io/dequbed)
This key is also used to sign the other tags so to verify them you can run e.g.
```
$ git tag -v keys/dequbed/xmpp+omemo
```
## Feature Requests ## Feature Requests
We also like new feature requests of course! We also like new feature requests of course!
But before you open an issue in this repo for a feature request, please first check a few things: But before you open an issue in this repo for a feature request, please first check a few things:
1. Is it a feature that needs to be implemented in more than just the backend server? For example,
is it something also having a GUI-component or something that you want to be able to do via the 1. Is it a feature that needs to be implemented in more than just the backend server? For example, is it something also having a GUI-component or something that you want to be able to do via the API? If so it's better suited over at the
API? If so it's better suited over at the [Lastenheft](https://gitlab.com/fabinfra/fabaccess_lastenheft) because that's where the required coordination for that will end up happening
[Lastenheft](https://gitlab.com/fabinfra/fabaccess_lastenheft) because that's where the required 2. Who else needs that feature? Is this something super specific to your environment/application or something that others will want too? If it's something that's relevant for more people please also tell us that in the feature request.
coordination for that will end up happening 3. Can you already get partway or all the way there using what's there already? If so please also tell us what you're currently doing and what doesn't work or why you dislike your current solution.
2. Who else needs that feature? Is this something super specific to your environment/application or
something that others will want too? If it's something that's relevant for more people please
also tell us that in the feature request.
3. Can you already get partway or all the way there using what's there already? If so please also
tell us what you're currently doing and what doesn't work or why you dislike your current
solution.
## Contributing Code ## Contributing Code
To help develop Diflouroborane you will need a Rust toolchain. I heavily recommend installing To help develop Difluoroborane you will need a Rust toolchain. We heavily recommend installing [rustup](https://rustup.rs) even if your distribution provides a recent enough rustc, simply because it allows to easily switch compilers between several versions of both stable and nightly. It also allows you to download the respective stdlib crate, giving you the option of an offline reference.
[rustup](https://rustup.rs) even if your distribution provides a recent enough rustc, simply because
it allows to easily switch compilers between several versions of both stable and nightly. It also
allows you to download the respective stdlib crate, giving you the option of an offline reference.
We use a stable release branch / moving development workflow. This means that all *new* development We use a stable release branch / moving development workflow. This means that all *new* development should happen on the `development` branch which is regularly merged into `stable` as releases. The exception of course are bug- and hotfixes that can target whichever branch.
should happen on the `development` branch which is regularly merged into `stable` as releases. The
exception of course are bug- and hotfixes that can target whichever branch.
If you want to add a new feature please work off the development branch. We suggest you create If you want to add a new feature please work off the development branch. We suggest you create yourself a feature branch, e.g. using
yourself a feature branch, e.g. using `git switch development; git checkout -b
feature/my-cool-feature`.
Using a feature branch keeps your local `development` branch clean, making it easier to later rebase
your feature branch onto it before you open a pull/merge request.
When you want feedback on your current progress or are ready to have it merged upstream open a merge ```git switch development; git checkout -b feature/my-cool-feature```
request. Don't worry we don't bite! ^^
Using a feature branch keeps your local `development` branch clean, making it easier to later rebase your feature branch onto it before you open a pull/merge request.
# Development Setup When you want feedback on your current progress or are ready to have it merged upstream open a merge request. Don't worry, we don't bite! ^^
## Cross-compilation
If you want to cross-compile you need both a C-toolchain for your target
and install the Rust stdlib for said target.
As an example for the target `aarch64-unknown-linux-gnu` (64-bit ARMv8
running Linux with the glibc, e.g. a Raspberry Pi 3 or later with a 64-bit
Debian Linux installation):
1. Install C-toolchain using your distro package manager:
- On Archlinux: `pacman -S aarch64-unknown-linux-gnu-gcc`
2. Install the Rust stdlib:
- using rustup: `rustup target add aarch64-unknown-linux-gnu`
3. Configure your cargo config:
### Configuring cargo
You need to tell Cargo to use your C-toolchain. For this you need to have
a block in [your user cargo config](https://doc.rust-lang.org/cargo/reference/config.html) setting at
least the paths to the gcc as `linker` and ar as `ar`:
```toml
[target.aarch64-unknown-linux-gnu]
# You must set the gcc as linker since a lot of magic must happen here.
linker = "aarch64-linux-gnu-gcc"
ar = "aarch64-linux-gnu-ar"
```
This block should be added to your **user** cargo config (usually
`~/.cargo/config.toml`), since these values can differ between distros and
users.
To actually compile for the given triple you need to call `cargo build`
with the `--target` flag:
```
$ cargo build --release --target=aarch64-unknown-linux-gnu
```
## Tests
Sadly, still very much `// TODO:`. We're working on it! :/

1354
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
[package] [package]
name = "diflouroborane" name = "difluoroborane"
version = "0.4.2" version = "0.4.2"
authors = [ "dequbed <me@dequbed.space>" authors = [ "dequbed <me@dequbed.space>"
, "Kai Jan Kriegel <kai@kjkriegel.de>" , "Kai Jan Kriegel <kai@kjkriegel.de>"
@ -66,7 +66,7 @@ ptr_meta = "0.1"
rkyv_typename = "0.7" rkyv_typename = "0.7"
rkyv_dyn = "0.7" rkyv_dyn = "0.7"
inventory = "0.1" inventory = "0.1"
linkme = "0.2.10" linkme = "0.3"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
# Password hashing for internal users # Password hashing for internal users
@ -84,7 +84,8 @@ capnp = "0.14"
capnp-rpc = "0.14.1" capnp-rpc = "0.14.1"
# API Authentication # API Authentication
desfire = "0.2.0-alpha1" desfire = "0.2.0-alpha3"
hex = { version = "0.4.3", features = ["serde"] } hex = { version = "0.4.3", features = ["serde"] }
futures-signals = "0.3.22" futures-signals = "0.3.22"
@ -112,10 +113,9 @@ rustls-native-certs = "0.6.1"
shadow-rs = "0.11" shadow-rs = "0.11"
[dependencies.rsasl] [dependencies.rsasl]
git = "https://github.com/dequbed/rsasl.git" version = "2.2.0"
rev = "0b5012d0"
default_features = false default_features = false
features = ["unstable_custom_mechanism", "provider", "registry_static", "plain"] features = ["unstable_custom_mechanism", "provider", "registry_static", "config_builder", "plain"]
[dev-dependencies] [dev-dependencies]
futures-test = "0.3.16" futures-test = "0.3.16"

View File

@ -1,21 +1,23 @@
# Setup build image for multistage build FROM --platform=$BUILDPLATFORM alpine:latest as copy
FROM rust:bullseye as builder ARG TARGETPLATFORM
# install build deps RUN case "$TARGETPLATFORM" in \
RUN apt-get update && apt-get upgrade -y "linux/arm/v7") echo armv7-unknown-linux-gnueabihf > /rust_target.txt ;; \
RUN apt-get install -yqq --no-install-recommends capnproto "linux/arm/v6") echo arm-unknown-linux-gnueabihf > /rust_target.txt ;; \
"linux/arm64") echo aarch64-unknown-linux-gnu > /rust_target.txt ;; \
"linux/amd64") echo x86_64-unknown-linux-gnu > /rust_target.txt ;; \
*) exit 1 ;; \
esac
WORKDIR /usr/src/bffh WORKDIR /usr/src/bffh
COPY . . COPY . .
RUN cargo build --release RUN cp target/$(cat /rust_target.txt)/release/bffhd ./bffhd.bin
# Setup deployable image # Setup deployable image
FROM debian:bullseye-slim FROM ubuntu:22.04
# Install runtime deps RUN apt-get update && apt-get upgrade -y
#RUN apt-get update && apt-get upgrade -yqq RUN apt-get install -yqq --no-install-recommends python3 python3-pip
COPY --from=builder /usr/src/bffh/target/release/bffhd /usr/local/bin/bffhd RUN pip3 install paho-mqtt
#COPY --from=builder /usr/src/bffh/examples/bffh.dhall /etc/diflouroborane.dhall COPY --from=copy /usr/src/bffh/bffhd.bin /usr/local/bin/bffhd
# RUN diflouroborane --print-default > /etc/diflouroborane.toml
VOLUME /etc/bffh/ VOLUME /etc/bffh/
VOLUME /var/lib/bffh/ VOLUME /var/lib/bffh/
VOLUME /usr/local/lib/bffh/adapters/ VOLUME /usr/local/lib/bffh/adapters/

View File

@ -1,53 +0,0 @@
# Installation
Currently there are no distribution packages available.
However installation is reasonably straight-forward, since Diflouroborane compiles into a single
mostly static binary with few dependencies.
At the moment only Linux is supported. If you managed to compile Diflouroborane please open an issue
outlining your steps or add a merge request expanding this part. Thanks!
## Requirements
General requirements; scroll down for distribution-specific instructions
- GNU SASL (libgsasl).
* If you want to compile Diflouroborane from source you will potentially also need development
headers
- capnproto
- rustc stable / nightly >= 1.48
* If your distribution does not provide a recent enough rustc, [rustup](https://rustup.rs/) helps
installing a local toolchain and keeping it up to date.
###### Arch Linux:
```shell
$ pacman -S gsasl rust capnproto
```
## Compiling from source
Diflouroborane uses Cargo, so compilation boils down to:
```shell
$ cargo build --release
```
The compiled binary can then be found in `./target/release/bffhd`
### Cross-compiling
If you need to compile for a different CPU target than your own (e.g. you want
to use BFFH on a raspberry pi but compile on your desktop PC), you need to
setup a cross-compilation toolchain and configure your Cargo correctly.
[The `CONTRIBUTING.md` has a section on how to setup a cross-compilation system.](CONTRIBUTING.md#cross-compilation)
# Running bffhd
The server can be ran either using `cargo`, which will also compile the binary if necessary, or directly.
When running using `cargo` you need to pass arguments to bffh after a `--`, so
e.g. `cargo run --release -- --help` or `cargo run --release -- -c examples/bffh.toml`.
When running directly the `bffhd` binary can be copied anywhere.
A list of arguments for the server is found in the help, so `bffhd --help` or `cargo run --release -- --help`.

View File

@ -1,8 +1,8 @@
# FabAccess Diflouroborane # FabAccess Difluoroborane
Diflouroborane (shorter: BFFH, the chemical formula for Diflouroborane) is the server part of Difluoroborane (shorter: BFFH, the chemical formula for Difluoroborane) is the server part of
FabAccess. FabAccess.
It provides a server-side implementation of the [FabAccess API](/fabinfra/fabaccess/fabaccess-api). It provides a server-side implementation of the [FabAccess API](https://gitlab.com/fabinfra/fabaccess/fabaccess-api).
## What is this? ## What is this?
@ -13,14 +13,14 @@ to be used for all other things one would like to give exclusive access to even
dangerous or expensive to use (think 3D printers, smart lightbulbs, meeting rooms). dangerous or expensive to use (think 3D printers, smart lightbulbs, meeting rooms).
FabAccess uses a Client/Server architecture with a [Cap'n Proto](https://capnproto.org/) API. You FabAccess uses a Client/Server architecture with a [Cap'n Proto](https://capnproto.org/) API. You
can find the API schema files over [in their own repository](/fabinfra/fabaccess/fabaccess-api). can find the API schema files over [in their own repository](https://gitlab.com/fabinfra/fabaccess/fabaccess-api).
The reference client is [Borepin](/fabinfra/fabaccess/borepin), written in C#/Xamarin to be able to The reference client is [Borepin](https://gitlab.com/fabinfra/fabaccess/borepin), written in C#/Xamarin to be able to
be ported to as many platforms as possible. be ported to as many platforms as possible.
## Installation ## Installation
See [INSTALL.md](INSTALL.md) See [https://fab-access.org/install](https://fab-access.org/install)
## Contributing ## Contributing

@ -1 +1 @@
Subproject commit 19f20f5154f0eced6288ff56cac840025ee51da1 Subproject commit f3f53dafb6b7d23a19947f2a32d4ed5ee4e91d22

View File

@ -12,9 +12,10 @@ use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
use miette::IntoDiagnostic; use miette::Diagnostic;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use std::time::Duration; use std::time::Duration;
use thiserror::Error;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use rumqttc::ConnectReturnCode::Success; use rumqttc::ConnectReturnCode::Success;
@ -111,11 +112,33 @@ static ROOT_CERTS: Lazy<RootCertStore> = Lazy::new(|| {
store store
}); });
pub fn load(executor: Executor, config: &Config, resources: ResourcesHandle) -> miette::Result<()> { #[derive(Debug, Error, Diagnostic)]
pub enum ActorError {
#[error("failed to parse MQTT url")]
UrlParseError(
#[from]
#[source]
url::ParseError,
),
#[error("MQTT config is invalid")]
InvalidConfig,
#[error("MQTT connection failed")]
ConnectionError(
#[from]
#[source]
rumqttc::ConnectionError,
),
}
pub fn load(
executor: Executor,
config: &Config,
resources: ResourcesHandle,
) -> Result<(), ActorError> {
let span = tracing::info_span!("loading actors"); let span = tracing::info_span!("loading actors");
let _guard = span; let _guard = span;
let mqtt_url = Url::parse(config.mqtt_url.as_str()).into_diagnostic()?; let mqtt_url = Url::parse(config.mqtt_url.as_str())?;
let (transport, default_port) = match mqtt_url.scheme() { let (transport, default_port) = match mqtt_url.scheme() {
"mqtts" | "ssl" => ( "mqtts" | "ssl" => (
rumqttc::Transport::tls_with_config( rumqttc::Transport::tls_with_config(
@ -132,12 +155,12 @@ pub fn load(executor: Executor, config: &Config, resources: ResourcesHandle) ->
scheme => { scheme => {
tracing::error!(%scheme, "MQTT url uses invalid scheme"); tracing::error!(%scheme, "MQTT url uses invalid scheme");
miette::bail!("invalid config"); return Err(ActorError::InvalidConfig);
} }
}; };
let host = mqtt_url.host_str().ok_or_else(|| { let host = mqtt_url.host_str().ok_or_else(|| {
tracing::error!("MQTT url must contain a hostname"); tracing::error!("MQTT url must contain a hostname");
miette::miette!("invalid config") ActorError::InvalidConfig
})?; })?;
let port = mqtt_url.port().unwrap_or(default_port); let port = mqtt_url.port().unwrap_or(default_port);
@ -168,7 +191,7 @@ pub fn load(executor: Executor, config: &Config, resources: ResourcesHandle) ->
} }
Err(error) => { Err(error) => {
tracing::error!(?error, "MQTT connection failed"); tracing::error!(?error, "MQTT connection failed");
miette::bail!("mqtt connection failed") return Err(ActorError::ConnectionError(error));
} }
} }

View File

@ -1,8 +1,10 @@
use miette::Diagnostic;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
use std::io; use std::io;
use std::io::{LineWriter, Write}; use std::io::{LineWriter, Write};
use std::sync::Mutex; use std::sync::Mutex;
use thiserror::Error;
use crate::Config; use crate::Config;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -23,8 +25,13 @@ pub struct AuditLogLine<'a> {
state: &'a str, state: &'a str,
} }
#[derive(Debug, Error, Diagnostic)]
#[error(transparent)]
#[repr(transparent)]
pub struct Error(#[from] pub io::Error);
impl AuditLog { impl AuditLog {
pub fn new(config: &Config) -> io::Result<&'static Self> { pub fn new(config: &Config) -> Result<&'static Self, Error> {
AUDIT.get_or_try_init(|| { AUDIT.get_or_try_init(|| {
tracing::debug!(path = %config.auditlog_path.display(), "Initializing audit log"); tracing::debug!(path = %config.auditlog_path.display(), "Initializing audit log");
let fd = OpenOptions::new() let fd = OpenOptions::new()

View File

@ -2,43 +2,37 @@ mod server;
pub use server::FabFire; pub use server::FabFire;
use rsasl::mechname::Mechname; use rsasl::mechname::Mechname;
use rsasl::registry::{Mechanism, MECHANISMS}; use rsasl::registry::{Matches, Mechanism, Named, Side, MECHANISMS};
use rsasl::session::Side;
const MECHNAME: &'static Mechname = &Mechname::const_new_unchecked(b"X-FABFIRE"); const MECHNAME: &'static Mechname = &Mechname::const_new_unchecked(b"X-FABFIRE");
#[linkme::distributed_slice(MECHANISMS)] #[linkme::distributed_slice(MECHANISMS)]
pub static FABFIRE: Mechanism = Mechanism { pub static FABFIRE: Mechanism = Mechanism::build(
mechanism: MECHNAME, MECHNAME,
priority: 300, 300,
// In this situation there's one struct for both sides, however you can just as well use None,
// different types than then have different `impl Authentication` instead of checking a value Some(FabFire::new_server),
// in self. Side::Client,
client: None, |_| Some(Matches::<Select>::name()),
server: Some(FabFire::new_server), |_| true,
first: Side::Client, );
};
use rsasl::property::{Property, PropertyDefinition, PropertyQ}; struct Select;
impl Named for Select {
fn mech() -> &'static Mechanism {
&FABFIRE
}
}
use rsasl::property::SizedProperty;
use std::marker::PhantomData; use std::marker::PhantomData;
// All Property types must implement Debug. // All Property types must implement Debug.
#[derive(Debug)] #[derive(Debug)]
// The `PhantomData` in the constructor is only used so external crates can't construct this type. // The `PhantomData` in the constructor is only used so external crates can't construct this type.
pub struct FabFireCardKey(PhantomData<()>); pub struct FabFireCardKey(PhantomData<()>);
impl PropertyQ for FabFireCardKey {
// This is the type stored for this property. This could also be the struct itself if you impl SizedProperty<'_> for FabFireCardKey {
// so choose type Value = [u8; 16];
type Item = [u8; 16]; const DESCRIPTION: &'static str = "A AES128 key for a FabFire card";
// You need to return the constant you define below here for things to work properly
fn property() -> Property {
FABFIRECARDKEY
}
} }
// This const is used by your mechanism to query and by your users to set your property. It
// thus needs to be exported from your crate
pub const FABFIRECARDKEY: Property = Property::new(&PropertyDefinition::new(
// Short name, used in `Debug` output
"FabFireCardKey",
// A longer user-facing name used in `Display` output
"A AES128 key for a FabFire card",
));

View File

@ -2,16 +2,16 @@ use desfire::desfire::desfire::MAX_BYTES_PER_TRANSACTION;
use desfire::desfire::Desfire; use desfire::desfire::Desfire;
use desfire::error::Error as DesfireError; use desfire::error::Error as DesfireError;
use desfire::iso7816_4::apduresponse::APDUResponse; use desfire::iso7816_4::apduresponse::APDUResponse;
use rsasl::error::{MechanismError, MechanismErrorKind, SASLError, SessionError}; use rsasl::mechanism::{
use rsasl::mechanism::Authentication; Authentication, Demand, DemandReply, MechanismData, MechanismError, MechanismErrorKind,
Provider, State, ThisProvider,
};
use rsasl::prelude::{MessageSent, SASLConfig, SASLError, SessionError};
use rsasl::property::AuthId; use rsasl::property::AuthId;
use rsasl::session::{SessionData, StepResult};
use rsasl::SASL;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::convert::TryFrom; use std::convert::TryFrom;
use std::fmt::{Debug, Display, Formatter}; use std::fmt::{Debug, Display, Formatter};
use std::io::Write; use std::io::Write;
use std::sync::Arc;
use crate::authentication::fabfire::FabFireCardKey; use crate::authentication::fabfire::FabFireCardKey;
@ -63,6 +63,8 @@ impl Display for FabFireError {
} }
} }
impl std::error::Error for FabFireError {}
impl MechanismError for FabFireError { impl MechanismError for FabFireError {
fn kind(&self) -> MechanismErrorKind { fn kind(&self) -> MechanismErrorKind {
match self { match self {
@ -89,6 +91,7 @@ struct CardInfo {
} }
struct KeyInfo { struct KeyInfo {
authid: String,
key_id: u8, key_id: u8,
key: Box<[u8]>, key: Box<[u8]>,
} }
@ -99,6 +102,7 @@ struct AuthInfo {
iv: Vec<u8>, iv: Vec<u8>,
} }
#[allow(non_camel_case_types)]
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "Cmd")] #[serde(tag = "Cmd")]
enum CardCommand { enum CardCommand {
@ -154,7 +158,7 @@ pub struct FabFire {
const MAGIC: &'static str = "FABACCESS\0DESFIRE\01.0\0"; const MAGIC: &'static str = "FABACCESS\0DESFIRE\01.0\0";
impl FabFire { impl FabFire {
pub fn new_server(_sasl: &SASL) -> Result<Box<dyn Authentication>, SASLError> { pub fn new_server(_sasl: &SASLConfig) -> Result<Box<dyn Authentication>, SASLError> {
Ok(Box::new(Self { Ok(Box::new(Self {
step: Step::New, step: Step::New,
card_info: None, card_info: None,
@ -174,10 +178,10 @@ impl FabFire {
impl Authentication for FabFire { impl Authentication for FabFire {
fn step( fn step(
&mut self, &mut self,
session: &mut SessionData, session: &mut MechanismData<'_, '_>,
input: Option<&[u8]>, input: Option<&[u8]>,
writer: &mut dyn Write, writer: &mut dyn Write,
) -> StepResult { ) -> Result<State, SessionError> {
match self.step { match self.step {
Step::New => { Step::New => {
tracing::trace!("Step: New"); tracing::trace!("Step: New");
@ -216,7 +220,7 @@ impl Authentication for FabFire {
writer writer
.write_all(&send_buf) .write_all(&send_buf)
.map_err(|e| SessionError::Io { source: e })?; .map_err(|e| SessionError::Io { source: e })?;
Ok(rsasl::session::Step::NeedsMore(Some(send_buf.len()))) Ok(State::Running)
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to serialize APDUCommand: {:?}", e); tracing::error!("Failed to serialize APDUCommand: {:?}", e);
@ -282,7 +286,7 @@ impl Authentication for FabFire {
writer writer
.write_all(&send_buf) .write_all(&send_buf)
.map_err(|e| SessionError::Io { source: e })?; .map_err(|e| SessionError::Io { source: e })?;
Ok(rsasl::session::Step::NeedsMore(Some(send_buf.len()))) Ok(State::Running)
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to serialize APDUCommand: {:?}", e); tracing::error!("Failed to serialize APDUCommand: {:?}", e);
@ -365,7 +369,7 @@ impl Authentication for FabFire {
writer writer
.write_all(&send_buf) .write_all(&send_buf)
.map_err(|e| SessionError::Io { source: e })?; .map_err(|e| SessionError::Io { source: e })?;
Ok(rsasl::session::Step::NeedsMore(Some(send_buf.len()))) Ok(State::Running)
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to serialize APDUCommand: {:?}", e); tracing::error!("Failed to serialize APDUCommand: {:?}", e);
@ -452,7 +456,7 @@ impl Authentication for FabFire {
writer writer
.write_all(&send_buf) .write_all(&send_buf)
.map_err(|e| SessionError::Io { source: e })?; .map_err(|e| SessionError::Io { source: e })?;
Ok(rsasl::session::Step::NeedsMore(Some(send_buf.len()))) Ok(State::Running)
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to serialize APDUCommand: {:?}", e); tracing::error!("Failed to serialize APDUCommand: {:?}", e);
@ -490,26 +494,20 @@ impl Authentication for FabFire {
Ok(_) => { Ok(_) => {
match apdu_response.body { match apdu_response.body {
Some(data) => { Some(data) => {
let token = String::from_utf8(data).unwrap(); let authid = String::from_utf8(data)
session.set_property::<AuthId>(Arc::new( .unwrap()
token.trim_matches(char::from(0)).to_string(), .trim_matches(char::from(0))
)); .to_string();
let key = match session.get_property_or_callback::<FabFireCardKey>() let prov = ThisProvider::<AuthId>::with(&authid);
{ let key = session
Ok(Some(key)) => Box::from(key.as_slice()), .need_with::<FabFireCardKey, _, _>(&prov, |key| {
Ok(None) => { Ok(Box::from(key.as_slice()))
tracing::error!("No keys on file for token"); })?;
return Err(FabFireError::InvalidCredentials( self.key_info = Some(KeyInfo {
"No keys on file for token".to_string(), authid,
) key_id: 0x01,
.into()); key,
} });
Err(e) => {
tracing::error!("Failed to get key: {:?}", e);
return Err(FabFireError::Session(e).into());
}
};
self.key_info = Some(KeyInfo { key_id: 0x01, key });
} }
None => { None => {
tracing::error!("No data in response"); tracing::error!("No data in response");
@ -546,7 +544,7 @@ impl Authentication for FabFire {
writer writer
.write_all(&send_buf) .write_all(&send_buf)
.map_err(|e| SessionError::Io { source: e })?; .map_err(|e| SessionError::Io { source: e })?;
Ok(rsasl::session::Step::NeedsMore(Some(send_buf.len()))) Ok(State::Running)
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to serialize command: {:?}", e); tracing::error!("Failed to serialize command: {:?}", e);
@ -616,7 +614,7 @@ impl Authentication for FabFire {
writer writer
.write_all(&send_buf) .write_all(&send_buf)
.map_err(|e| SessionError::Io { source: e })?; .map_err(|e| SessionError::Io { source: e })?;
Ok(rsasl::session::Step::NeedsMore(Some(send_buf.len()))) Ok(State::Running)
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to serialize command: {:?}", e); tracing::error!("Failed to serialize command: {:?}", e);
@ -691,9 +689,26 @@ impl Authentication for FabFire {
writer writer
.write_all(&send_buf) .write_all(&send_buf)
.map_err(|e| SessionError::Io { source: e })?; .map_err(|e| SessionError::Io { source: e })?;
return Ok(rsasl::session::Step::Done(Some(
send_buf.len(), struct Prov<'a> {
))); authid: &'a str,
}
impl<'a> Provider<'a> for Prov<'a> {
fn provide(
&self,
req: &mut Demand<'a>,
) -> DemandReply<()>
{
req.provide_ref::<AuthId>(self.authid)?
.done()
}
}
let prov = Prov {
authid: &self.key_info.as_ref().unwrap().authid,
};
session.validate(&prov)?;
return Ok(State::Finished(MessageSent::Yes));
} }
Err(e) => { Err(e) => {
tracing::error!( tracing::error!(
@ -722,6 +737,6 @@ impl Authentication for FabFire {
} }
} }
return Ok(rsasl::session::Step::Done(None)); return Ok(State::Finished(MessageSent::No));
} }
} }

View File

@ -0,0 +1,25 @@
mod server;
pub use server::FabFire;
use rsasl::mechname::Mechname;
use rsasl::registry::{Matches, Mechanism, Named, Side, MECHANISMS};
const MECHNAME: &'static Mechname = &Mechname::const_new_unchecked(b"X-FABFIRE-BIN");
#[linkme::distributed_slice(MECHANISMS)]
pub static FABFIRE: Mechanism = Mechanism::build(
MECHNAME,
300,
None,
Some(FabFire::new_server),
Side::Client,
|_| Some(Matches::<Select>::name()),
|_| true,
);
struct Select;
impl Named for Select {
fn mech() -> &'static Mechanism {
&FABFIRE
}
}

View File

@ -0,0 +1,532 @@
use desfire::desfire::desfire::MAX_BYTES_PER_TRANSACTION;
use desfire::desfire::Desfire;
use desfire::error::Error as DesfireError;
use desfire::iso7816_4::apduresponse::APDUResponse;
use rsasl::mechanism::{
Authentication, Demand, DemandReply, MechanismData, MechanismError, MechanismErrorKind,
Provider, State, ThisProvider,
};
use rsasl::prelude::{MessageSent, SASLConfig, SASLError, SessionError};
use rsasl::property::AuthId;
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use std::fmt::{Debug, Display, Formatter};
use std::io::Write;
use crate::authentication::fabfire::FabFireCardKey;
use crate::CONFIG;
enum FabFireError {
ParseError,
SerializationError,
DeserializationError(serde_json::Error),
CardError(DesfireError),
InvalidMagic(String),
InvalidToken(String),
InvalidURN(String),
InvalidCredentials(String),
Session(SessionError),
}
impl Debug for FabFireError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
FabFireError::ParseError => write!(f, "ParseError"),
FabFireError::SerializationError => write!(f, "SerializationError"),
FabFireError::DeserializationError(e) => write!(f, "DeserializationError: {}", e),
FabFireError::CardError(err) => write!(f, "CardError: {}", err),
FabFireError::InvalidMagic(magic) => write!(f, "InvalidMagic: {}", magic),
FabFireError::InvalidToken(token) => write!(f, "InvalidToken: {}", token),
FabFireError::InvalidURN(urn) => write!(f, "InvalidURN: {}", urn),
FabFireError::InvalidCredentials(credentials) => {
write!(f, "InvalidCredentials: {}", credentials)
}
FabFireError::Session(err) => write!(f, "Session: {}", err),
}
}
}
impl Display for FabFireError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
FabFireError::ParseError => write!(f, "ParseError"),
FabFireError::SerializationError => write!(f, "SerializationError"),
FabFireError::DeserializationError(e) => write!(f, "DeserializationError: {}", e),
FabFireError::CardError(err) => write!(f, "CardError: {}", err),
FabFireError::InvalidMagic(magic) => write!(f, "InvalidMagic: {}", magic),
FabFireError::InvalidToken(token) => write!(f, "InvalidToken: {}", token),
FabFireError::InvalidURN(urn) => write!(f, "InvalidURN: {}", urn),
FabFireError::InvalidCredentials(credentials) => {
write!(f, "InvalidCredentials: {}", credentials)
}
FabFireError::Session(err) => write!(f, "Session: {}", err),
}
}
}
impl std::error::Error for FabFireError {}
impl MechanismError for FabFireError {
fn kind(&self) -> MechanismErrorKind {
match self {
FabFireError::ParseError => MechanismErrorKind::Parse,
FabFireError::SerializationError => MechanismErrorKind::Protocol,
FabFireError::DeserializationError(_) => MechanismErrorKind::Parse,
FabFireError::CardError(_) => MechanismErrorKind::Protocol,
FabFireError::InvalidMagic(_) => MechanismErrorKind::Protocol,
FabFireError::InvalidToken(_) => MechanismErrorKind::Protocol,
FabFireError::InvalidURN(_) => MechanismErrorKind::Protocol,
FabFireError::InvalidCredentials(_) => MechanismErrorKind::Protocol,
FabFireError::Session(_) => MechanismErrorKind::Protocol,
}
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct CardInfo {
#[serde(rename = "UID", with = "hex")]
uid: [u8; 7],
key_old: Option<Box<[u8]>>,
key_new: Option<Box<[u8]>>,
}
struct KeyInfo {
authid: String,
key_id: u8,
key: Box<[u8]>,
}
struct AuthInfo {
rnd_a: Vec<u8>,
rnd_b: Vec<u8>,
iv: Vec<u8>,
}
enum Step {
New,
SelectApp,
VerifyMagic,
GetURN,
GetToken,
Authenticate1,
Authenticate2,
}
pub struct FabFire {
step: Step,
card_info: Option<CardInfo>,
key_info: Option<KeyInfo>,
auth_info: Option<AuthInfo>,
app_id: u32,
local_urn: String,
desfire: Desfire,
}
const MAGIC: &'static str = "FABACCESS\0DESFIRE\01.0\0";
impl FabFire {
pub fn new_server(_sasl: &SASLConfig) -> Result<Box<dyn Authentication>, SASLError> {
let space = if let Some(space) = CONFIG.get().map(|c| c.spacename.as_str()) {
space
} else {
tracing::error!("No space configured");
"generic"
};
Ok(Box::new(Self {
step: Step::New,
card_info: None,
key_info: None,
auth_info: None,
app_id: 0x464142,
local_urn: format!("urn:fabaccess:lab:{space}"),
desfire: Desfire {
card: None,
session_key: None,
cbc_iv: None,
},
}))
}
}
impl Authentication for FabFire {
fn step(
&mut self,
session: &mut MechanismData<'_, '_>,
input: Option<&[u8]>,
writer: &mut dyn Write,
) -> Result<State, SessionError> {
match self.step {
Step::New => {
tracing::trace!("Step: New");
//receive card info (especially card UID) from reader
return match input {
None => Err(SessionError::InputDataRequired),
Some(_) => {
//select application
return match self.desfire.select_application_cmd(self.app_id) {
Ok(buf) => match Vec::<u8>::try_from(buf) {
Ok(data) => {
self.step = Step::SelectApp;
writer
.write_all(&data)
.map_err(|e| SessionError::Io { source: e })?;
Ok(State::Running)
}
Err(e) => {
tracing::error!(
"Failed to convert APDUCommand to Vec<u8>: {:?}",
e
);
return Err(FabFireError::SerializationError.into());
}
},
Err(e) => {
tracing::error!("Failed to generate APDUCommand: {:?}", e);
return Err(FabFireError::SerializationError.into());
}
};
}
};
}
Step::SelectApp => {
tracing::trace!("Step: SelectApp");
// check that we successfully selected the application
let apdu_response = match input {
Some(data) => APDUResponse::new(data),
None => return Err(SessionError::InputDataRequired),
};
apdu_response
.check()
.map_err(|e| FabFireError::CardError(e))?;
// request the contents of the file containing the magic string
const MAGIC_FILE_ID: u8 = 0x01;
return match self
.desfire
.read_data_chunk_cmd(MAGIC_FILE_ID, 0, MAGIC.len())
{
Ok(buf) => match Vec::<u8>::try_from(buf) {
Ok(data) => {
self.step = Step::VerifyMagic;
writer
.write_all(&data)
.map_err(|e| SessionError::Io { source: e })?;
Ok(State::Running)
}
Err(e) => {
tracing::error!("Failed to convert APDUCommand to Vec<u8>: {:?}", e);
return Err(FabFireError::SerializationError.into());
}
},
Err(e) => {
tracing::error!("Failed to generate APDUCommand: {:?}", e);
return Err(FabFireError::SerializationError.into());
}
};
}
Step::VerifyMagic => {
tracing::trace!("Step: VerifyMagic");
// verify the magic string to determine that we have a valid fabfire card
let apdu_response = match input {
Some(data) => APDUResponse::new(data),
None => return Err(SessionError::InputDataRequired),
};
match apdu_response.check() {
Ok(_) => {
match apdu_response.body {
Some(data) => {
if std::str::from_utf8(data.as_slice()) != Ok(MAGIC) {
tracing::error!("Invalid magic string");
return Err(FabFireError::ParseError.into());
}
}
None => {
tracing::error!("No data returned from card");
return Err(FabFireError::ParseError.into());
}
};
}
Err(e) => {
tracing::error!("Got invalid APDUResponse: {:?}", e);
return Err(FabFireError::ParseError.into());
}
}
// request the contents of the file containing the URN
const URN_FILE_ID: u8 = 0x02;
return match self.desfire.read_data_chunk_cmd(
URN_FILE_ID,
0,
self.local_urn.as_bytes().len(),
) {
// TODO: support urn longer than 47 Bytes
Ok(buf) => match Vec::<u8>::try_from(buf) {
Ok(data) => {
self.step = Step::GetURN;
writer
.write_all(&data)
.map_err(|e| SessionError::Io { source: e })?;
Ok(State::Running)
}
Err(e) => {
tracing::error!("Failed to convert APDUCommand to Vec<u8>: {:?}", e);
return Err(FabFireError::SerializationError.into());
}
},
Err(e) => {
tracing::error!("Failed to generate APDUCommand: {:?}", e);
return Err(FabFireError::SerializationError.into());
}
};
}
Step::GetURN => {
tracing::trace!("Step: GetURN");
// parse the urn and match it to our local urn
let apdu_response = match input {
Some(data) => APDUResponse::new(data),
None => return Err(SessionError::InputDataRequired),
};
match apdu_response.check() {
Ok(_) => {
match apdu_response.body {
Some(data) => {
let received_urn = String::from_utf8(data).unwrap();
if received_urn != self.local_urn {
tracing::error!(
"URN mismatch: {:?} != {:?}",
received_urn,
self.local_urn
);
return Err(FabFireError::ParseError.into());
}
}
None => {
tracing::error!("No data returned from card");
return Err(FabFireError::ParseError.into());
}
};
}
Err(e) => {
tracing::error!("Got invalid APDUResponse: {:?}", e);
return Err(FabFireError::ParseError.into());
}
}
// request the contents of the file containing the URN
const TOKEN_FILE_ID: u8 = 0x03;
return match self.desfire.read_data_chunk_cmd(
TOKEN_FILE_ID,
0,
MAX_BYTES_PER_TRANSACTION,
) {
// TODO: support data longer than 47 Bytes
Ok(buf) => match Vec::<u8>::try_from(buf) {
Ok(data) => {
self.step = Step::GetToken;
writer
.write_all(&data)
.map_err(|e| SessionError::Io { source: e })?;
Ok(State::Running)
}
Err(e) => {
tracing::error!("Failed to convert APDUCommand to Vec<u8>: {:?}", e);
return Err(FabFireError::SerializationError.into());
}
},
Err(e) => {
tracing::error!("Failed to generate APDUCommand: {:?}", e);
return Err(FabFireError::SerializationError.into());
}
};
}
Step::GetToken => {
// println!("Step: GetToken");
// parse the token and select the appropriate user
let apdu_response = match input {
Some(data) => APDUResponse::new(data),
None => return Err(SessionError::InputDataRequired),
};
match apdu_response.check() {
Ok(_) => {
match apdu_response.body {
Some(data) => {
let authid = String::from_utf8(data)
.unwrap()
.trim_matches(char::from(0))
.to_string();
let prov = ThisProvider::<AuthId>::with(&authid);
let key = session
.need_with::<FabFireCardKey, _, _>(&prov, |key| {
Ok(Box::from(key.as_slice()))
})?;
self.key_info = Some(KeyInfo {
authid,
key_id: 0x01,
key,
});
}
None => {
tracing::error!("No data in response");
return Err(FabFireError::ParseError.into());
}
};
}
Err(e) => {
tracing::error!("Failed to check response: {:?}", e);
return Err(FabFireError::ParseError.into());
}
}
return match self
.desfire
.authenticate_iso_aes_challenge_cmd(self.key_info.as_ref().unwrap().key_id)
{
Ok(buf) => match Vec::<u8>::try_from(buf) {
Ok(data) => {
self.step = Step::Authenticate1;
writer
.write_all(&data)
.map_err(|e| SessionError::Io { source: e })?;
Ok(State::Running)
}
Err(e) => {
tracing::error!("Failed to convert to Vec<u8>: {:?}", e);
return Err(FabFireError::SerializationError.into());
}
},
Err(e) => {
tracing::error!("Failed to create authenticate command: {:?}", e);
return Err(FabFireError::SerializationError.into());
}
};
}
Step::Authenticate1 => {
tracing::trace!("Step: Authenticate1");
let apdu_response = match input {
Some(data) => APDUResponse::new(data),
None => return Err(SessionError::InputDataRequired),
};
return match apdu_response.check() {
Ok(_) => {
match apdu_response.body {
Some(data) => {
let rnd_b_enc = data.as_slice();
//FIXME: This is ugly, we should find a better way to make the function testable
//TODO: Check if we need a CSPRNG here
let rnd_a: [u8; 16] = rand::random();
let (cmd_challenge_response, rnd_b, iv) = self
.desfire
.authenticate_iso_aes_response_cmd(
rnd_b_enc,
&*(self.key_info.as_ref().unwrap().key),
&rnd_a,
)
.unwrap();
self.auth_info = Some(AuthInfo {
rnd_a: Vec::<u8>::from(rnd_a),
rnd_b,
iv,
});
match Vec::<u8>::try_from(cmd_challenge_response) {
Ok(data) => {
self.step = Step::Authenticate2;
writer
.write_all(&data)
.map_err(|e| SessionError::Io { source: e })?;
Ok(State::Running)
}
Err(e) => {
tracing::error!("Failed to convert to Vec<u8>: {:?}", e);
return Err(FabFireError::SerializationError.into());
}
}
}
None => {
tracing::error!("Got invalid response: {:?}", apdu_response);
Err(FabFireError::ParseError.into())
}
}
}
Err(e) => {
tracing::error!("Failed to check response: {:?}", e);
Err(FabFireError::ParseError.into())
}
};
}
Step::Authenticate2 => {
// println!("Step: Authenticate2");
let apdu_response = match input {
Some(data) => APDUResponse::new(data),
None => return Err(SessionError::InputDataRequired),
};
match apdu_response.check() {
Ok(_) => {
match apdu_response.body {
Some(data) => match self.auth_info.as_ref() {
None => {
return Err(FabFireError::ParseError.into());
}
Some(auth_info) => {
if self
.desfire
.authenticate_iso_aes_verify(
data.as_slice(),
auth_info.rnd_a.as_slice(),
auth_info.rnd_b.as_slice(),
&*(self.key_info.as_ref().unwrap().key),
auth_info.iv.as_slice(),
)
.is_ok()
{
struct Prov<'a> {
authid: &'a str,
}
impl<'a> Provider<'a> for Prov<'a> {
fn provide(
&self,
req: &mut Demand<'a>,
) -> DemandReply<()>
{
req.provide_ref::<AuthId>(self.authid)?.done()
}
}
let prov = Prov {
authid: &self.key_info.as_ref().unwrap().authid,
};
session.validate(&prov)?;
return Ok(State::Finished(MessageSent::Yes));
}
}
},
None => {
tracing::error!("got empty response");
return Err(FabFireError::ParseError.into());
}
};
}
Err(_e) => {
tracing::error!("Got invalid response: {:?}", apdu_response);
return Err(
FabFireError::InvalidCredentials(format!("{}", apdu_response)).into(),
);
}
}
}
}
return Ok(State::Finished(MessageSent::No));
}
}

View File

@ -1,16 +1,17 @@
use crate::users::Users; use crate::users::Users;
use miette::{Context, IntoDiagnostic}; use miette::{IntoDiagnostic, WrapErr};
use rsasl::error::SessionError; use rsasl::callback::{CallbackError, Context, Request, SessionCallback, SessionData};
use rsasl::mechname::Mechname; use rsasl::mechanism::SessionError;
use rsasl::property::{AuthId, Password}; use rsasl::prelude::{Mechname, SASLConfig, SASLServer, Session, Validation};
use rsasl::session::{Session, SessionData}; use rsasl::property::{AuthId, AuthzId, Password};
use rsasl::validate::{validations, Validation}; use rsasl::validate::{Validate, ValidationError};
use rsasl::{Property, SASL};
use std::sync::Arc; use std::sync::Arc;
use crate::authentication::fabfire::FabFireCardKey; use crate::authentication::fabfire::FabFireCardKey;
use crate::users::db::User;
mod fabfire; mod fabfire;
mod fabfire_bin;
struct Callback { struct Callback {
users: Users, users: Users,
@ -22,89 +23,102 @@ impl Callback {
Self { users, span } Self { users, span }
} }
} }
impl rsasl::callback::Callback for Callback { impl SessionCallback for Callback {
fn provide_prop( fn callback(
&self, &self,
session: &mut rsasl::session::SessionData, _session_data: &SessionData,
property: Property, context: &Context,
request: &mut Request,
) -> Result<(), SessionError> { ) -> Result<(), SessionError> {
match property { if let Some(authid) = context.get_ref::<AuthId>() {
fabfire::FABFIRECARDKEY => { request.satisfy_with::<FabFireCardKey, _>(|| {
let authcid = session.get_property_or_callback::<AuthId>()?; let user = self.users.get_user(authid).ok_or(CallbackError::NoValue)?;
let user = self
.users
.get_user(authcid.unwrap().as_ref())
.ok_or(SessionError::AuthenticationFailure)?;
let kv = user let kv = user
.userdata .userdata
.kv .kv
.get("cardkey") .get("cardkey")
.ok_or(SessionError::AuthenticationFailure)?; .ok_or(CallbackError::NoValue)?;
let card_key = <[u8; 16]>::try_from( let card_key =
hex::decode(kv).map_err(|_| SessionError::AuthenticationFailure)?, <[u8; 16]>::try_from(hex::decode(kv).map_err(|_| CallbackError::NoValue)?)
) .map_err(|_| CallbackError::NoValue)?;
.map_err(|_| SessionError::AuthenticationFailure)?; Ok(card_key)
session.set_property::<FabFireCardKey>(Arc::new(card_key)); })?;
Ok(())
}
_ => Err(SessionError::NoProperty { property }),
} }
Ok(())
} }
fn validate( fn validate(
&self, &self,
session: &mut SessionData, session_data: &SessionData,
validation: Validation, context: &Context,
_mechanism: &Mechname, validate: &mut Validate<'_>,
) -> Result<(), SessionError> { ) -> Result<(), ValidationError> {
let span = tracing::info_span!(parent: &self.span, "validate"); let span = tracing::info_span!(parent: &self.span, "validate");
let _guard = span.enter(); let _guard = span.enter();
match validation { if validate.is::<V>() {
validations::SIMPLE => { match session_data.mechanism().mechanism.as_str() {
let authnid = session "PLAIN" => {
.get_property::<AuthId>() let authcid = context
.ok_or(SessionError::no_property::<AuthId>())?; .get_ref::<AuthId>()
tracing::debug!(authid=%authnid, "SIMPLE validation requested"); .ok_or(ValidationError::MissingRequiredProperty)?;
let authzid = context
.get_ref::<AuthzId>()
.ok_or(ValidationError::MissingRequiredProperty)?;
let password = context
.get_ref::<Password>()
.ok_or(ValidationError::MissingRequiredProperty)?;
if let Some(user) = self.users.get_user(authnid.as_str()) { if !authzid.is_empty() {
let passwd = session
.get_property::<Password>()
.ok_or(SessionError::no_property::<Password>())?;
if user
.check_password(passwd.as_bytes())
.map_err(|_e| SessionError::AuthenticationFailure)?
{
return Ok(()); return Ok(());
} else {
tracing::warn!(authid=%authnid, "AUTH FAILED: bad password");
} }
} else {
tracing::warn!(authid=%authnid, "AUTH FAILED: no such user '{}'", authnid);
}
Err(SessionError::AuthenticationFailure) if let Some(user) = self.users.get_user(authcid) {
} match user.check_password(password) {
_ => { Ok(true) => validate.finalize::<V>(user),
tracing::error!(?validation, "Unimplemented validation requested"); Ok(false) => {
Err(SessionError::no_validate(validation)) tracing::warn!(authid=%authcid, "AUTH FAILED: bad password");
}
Err(error) => {
tracing::warn!(authid=%authcid, "Bad DB entry: {}", error);
}
}
} else {
tracing::warn!(authid=%authcid, "AUTH FAILED: no such user");
}
}
"X-FABFIRE" | "X-FABFIRE-BIN" => {
let authcid = context
.get_ref::<AuthId>()
.ok_or(ValidationError::MissingRequiredProperty)?;
if let Some(user) = self.users.get_user(authcid) {
validate.finalize::<V>(user)
}
}
_ => {}
} }
} }
Ok(())
} }
} }
pub struct V;
impl Validation for V {
type Value = User;
}
#[derive(Clone)]
struct Inner { struct Inner {
rsasl: SASL, rsasl: Arc<SASLConfig>,
} }
impl Inner { impl Inner {
pub fn new(rsasl: SASL) -> Self { pub fn new(rsasl: Arc<SASLConfig>) -> Self {
Self { rsasl } Self { rsasl }
} }
} }
#[derive(Clone)] #[derive(Clone)]
pub struct AuthenticationHandle { pub struct AuthenticationHandle {
inner: Arc<Inner>, inner: Inner,
} }
impl AuthenticationHandle { impl AuthenticationHandle {
@ -112,11 +126,13 @@ impl AuthenticationHandle {
let span = tracing::debug_span!("authentication"); let span = tracing::debug_span!("authentication");
let _guard = span.enter(); let _guard = span.enter();
let mut rsasl = SASL::new(); let config = SASLConfig::builder()
rsasl.install_callback(Arc::new(Callback::new(userdb))); .with_defaults()
.with_callback(Callback::new(userdb))
.unwrap();
let mechs: Vec<&'static str> = rsasl let mechs: Vec<&'static str> = SASLServer::<V>::new(config.clone())
.server_mech_list() .get_available()
.into_iter() .into_iter()
.map(|m| m.mechanism.as_str()) .map(|m| m.mechanism.as_str())
.collect(); .collect();
@ -124,24 +140,18 @@ impl AuthenticationHandle {
tracing::debug!(?mechs, "available mechs"); tracing::debug!(?mechs, "available mechs");
Self { Self {
inner: Arc::new(Inner::new(rsasl)), inner: Inner::new(config),
} }
} }
pub fn start(&self, mechanism: &Mechname) -> miette::Result<Session> { pub fn start(&self, mechanism: &Mechname) -> miette::Result<Session<V>> {
Ok(self Ok(SASLServer::new(self.inner.rsasl.clone())
.inner .start_suggested(mechanism)
.rsasl
.server_start(mechanism)
.into_diagnostic() .into_diagnostic()
.wrap_err("Failed to start a SASL authentication with the given mechanism")?) .wrap_err("Failed to start a SASL authentication with the given mechanism")?)
} }
pub fn list_available_mechs(&self) -> impl IntoIterator<Item = &Mechname> { pub fn sess(&self) -> SASLServer<V> {
self.inner SASLServer::new(self.inner.rsasl.clone())
.rsasl
.server_mech_list()
.into_iter()
.map(|m| m.mechanism)
} }
} }

View File

@ -2,13 +2,13 @@ use capnp::capability::Promise;
use capnp::Error; use capnp::Error;
use capnp_rpc::pry; use capnp_rpc::pry;
use rsasl::mechname::Mechname; use rsasl::mechname::Mechname;
use rsasl::property::AuthId; use rsasl::prelude::State as SaslState;
use rsasl::session::{Session, Step}; use rsasl::prelude::{MessageSent, Session};
use std::fmt; use std::fmt;
use std::fmt::{Formatter, Write}; use std::fmt::{Formatter, Write};
use std::io::Cursor;
use tracing::Span; use tracing::Span;
use crate::authentication::V;
use crate::capnp::session::APISession; use crate::capnp::session::APISession;
use crate::session::SessionManager; use crate::session::SessionManager;
use api::authenticationsystem_capnp::authentication::{ use api::authenticationsystem_capnp::authentication::{
@ -27,7 +27,7 @@ impl Authentication {
pub fn new( pub fn new(
parent: &Span, parent: &Span,
mechanism: &Mechname, /* TODO: this is stored in session as well, get it out of there. */ mechanism: &Mechname, /* TODO: this is stored in session as well, get it out of there. */
session: Session, session: Session<V>,
sessionmanager: SessionManager, sessionmanager: SessionManager,
) -> Self { ) -> Self {
let span = tracing::info_span!( let span = tracing::info_span!(
@ -92,7 +92,7 @@ enum State {
InvalidMechanism, InvalidMechanism,
Finished, Finished,
Aborted, Aborted,
Running(Session, SessionManager), Running(Session<V>, SessionManager),
} }
impl AuthenticationSystem for Authentication { impl AuthenticationSystem for Authentication {
@ -113,7 +113,7 @@ impl AuthenticationSystem for Authentication {
f.write_char(')') f.write_char(')')
} }
} }
let mut response; let response;
let mut builder = results.get(); let mut builder = results.get();
if let State::Running(mut session, manager) = if let State::Running(mut session, manager) =
@ -121,36 +121,35 @@ impl AuthenticationSystem for Authentication {
{ {
let data: &[u8] = pry!(pry!(params.get()).get_data()); let data: &[u8] = pry!(pry!(params.get()).get_data());
let mut out = Cursor::new(Vec::new()); let mut out = Vec::new();
match session.step(Some(data), &mut out) { match session.step(Some(data), &mut out) {
Ok(Step::Done(data)) => { Ok(SaslState::Finished(sent)) => {
self.state = State::Finished; self.state = State::Finished;
let uid = pry!(session.get_property::<AuthId>().ok_or_else(|| { if let Some(user) = session.validation() {
tracing::warn!("Authentication didn't provide an authid as required."); let session = manager.open(&self.span, user);
capnp::Error::failed( response = Response {
"Authentication didn't provide an authid as required".to_string(), union_field: "successful",
) };
}));
let session = pry!(manager.open(&self.span, uid.as_ref()).ok_or_else(|| {
tracing::warn!(uid = uid.as_str(), "Failed to lookup the given user");
capnp::Error::failed("Failed to lookup the given user".to_string())
}));
response = Response { let mut builder = builder.init_successful();
union_field: "successful", if sent == MessageSent::Yes {
}; builder.set_additional_data(out.as_slice());
}
let mut builder = builder.init_successful(); APISession::build(session, builder)
if data.is_some() { } else {
builder.set_additional_data(out.into_inner().as_slice()); let mut builder = builder.init_failed();
builder.set_code(ErrorCode::InvalidCredentials);
response = Response {
union_field: "error",
};
} }
APISession::build(session, builder)
} }
Ok(Step::NeedsMore(_)) => { Ok(SaslState::Running) => {
self.state = State::Running(session, manager); self.state = State::Running(session, manager);
builder.set_challenge(out.into_inner().as_slice()); builder.set_challenge(out.as_slice());
response = Response { response = Response {
union_field: "challenge", union_field: "challenge",

View File

@ -95,9 +95,10 @@ impl bootstrap::Server for BootCap {
let builder = result.get(); let builder = result.get();
let mechs: Vec<_> = self let mechs: Vec<_> = self
.authentication .authentication
.list_available_mechs() .sess()
.get_available()
.into_iter() .into_iter()
.map(|m| m.as_str()) .map(|m| m.mechanism.as_str())
.collect(); .collect();
let mut mechbuilder = builder.init_mechs(mechs.len() as u32); let mut mechbuilder = builder.init_mechs(mechs.len() as u32);
for (i, m) in mechs.iter().enumerate() { for (i, m) in mechs.iter().enumerate() {
@ -146,7 +147,7 @@ impl bootstrap::Server for BootCap {
tracing::trace!(params.mechanism = mechanism, "method call"); tracing::trace!(params.mechanism = mechanism, "method call");
let mechname = Mechname::new(mechanism.as_bytes()); let mechname = Mechname::parse(mechanism.as_bytes());
let auth = if let Ok(mechname) = mechname { let auth = if let Ok(mechname) = mechname {
if let Ok(session) = self.authentication.start(mechname) { if let Ok(session) = self.authentication.start(mechname) {
Authentication::new(&self.span, mechname, session, self.sessionmanager.clone()) Authentication::new(&self.span, mechname, session, self.sessionmanager.clone())

View File

@ -211,7 +211,6 @@ impl ManageServer for Machine {
mut result: manage::GetMachineInfoExtendedResults, mut result: manage::GetMachineInfoExtendedResults,
) -> Promise<(), ::capnp::Error> { ) -> Promise<(), ::capnp::Error> {
let mut builder = result.get(); let mut builder = result.get();
let user = User::new_self(self.session.clone());
User::build_optional( User::build_optional(
&self.session, &self.session,
self.resource.get_current_user(), self.resource.get_current_user(),

View File

@ -1,13 +1,15 @@
use async_net::TcpListener; use miette::Diagnostic;
use thiserror::Error;
use async_net::TcpListener;
use capnp_rpc::rpc_twoparty_capnp::Side; use capnp_rpc::rpc_twoparty_capnp::Side;
use capnp_rpc::twoparty::VatNetwork; use capnp_rpc::twoparty::VatNetwork;
use capnp_rpc::RpcSystem; use capnp_rpc::RpcSystem;
use executor::prelude::{Executor, GroupId, SupervisionRegistry}; use executor::prelude::{Executor, SupervisionRegistry};
use futures_rustls::server::TlsStream; use futures_rustls::server::TlsStream;
use futures_rustls::TlsAcceptor; use futures_rustls::TlsAcceptor;
use futures_util::stream::FuturesUnordered; use futures_util::stream::FuturesUnordered;
use futures_util::{stream, AsyncRead, AsyncWrite, FutureExt, StreamExt}; use futures_util::{stream, AsyncRead, AsyncWrite, StreamExt};
use std::future::Future; use std::future::Future;
use std::io; use std::io;
@ -37,6 +39,10 @@ pub struct APIServer {
authentication: AuthenticationHandle, authentication: AuthenticationHandle,
} }
#[derive(Debug, Error, Diagnostic)]
#[error("Reached Void error, this should not be possible")]
pub enum Error {}
impl APIServer { impl APIServer {
pub fn new( pub fn new(
executor: Executor<'static>, executor: Executor<'static>,
@ -60,7 +66,7 @@ impl APIServer {
acceptor: TlsAcceptor, acceptor: TlsAcceptor,
sessionmanager: SessionManager, sessionmanager: SessionManager,
authentication: AuthenticationHandle, authentication: AuthenticationHandle,
) -> miette::Result<Self> { ) -> Result<Self, Error> {
let span = tracing::info_span!("binding API listen sockets"); let span = tracing::info_span!("binding API listen sockets");
let _guard = span.enter(); let _guard = span.enter();

View File

@ -1,4 +1,3 @@
use crate::authorization::roles::Role;
use crate::Roles; use crate::Roles;
use api::permissionsystem_capnp::permission_system::info::{ use api::permissionsystem_capnp::permission_system::info::{
GetRoleListParams, GetRoleListResults, Server as PermissionSystem, GetRoleListParams, GetRoleListResults, Server as PermissionSystem,
@ -37,7 +36,7 @@ impl PermissionSystem for Permissions {
tracing::trace!("method call"); tracing::trace!("method call");
let roles = self.roles.list().collect::<Vec<&String>>(); let roles = self.roles.list().collect::<Vec<&String>>();
let mut builder = results.get(); let builder = results.get();
let mut b = builder.init_role_list(roles.len() as u32); let mut b = builder.init_role_list(roles.len() as u32);
for (i, role) in roles.into_iter().enumerate() { for (i, role) in roles.into_iter().enumerate() {
let mut role_builder = b.reborrow().get(i as u32); let mut role_builder = b.reborrow().get(i as u32);

View File

@ -1,20 +1,38 @@
use crate::authorization::permissions::Permission; use crate::authorization::permissions::Permission;
use crate::session::SessionHandle; use crate::session::SessionHandle;
use crate::users::{db, UserRef}; use crate::users::{db, UserRef};
use crate::CONFIG;
use api::general_capnp::optional; use api::general_capnp::optional;
use api::user_capnp::user::{self, admin, info, manage}; use api::user_capnp::user::card_d_e_s_fire_e_v2::{
BindParams, BindResults, GenCardTokenParams, GenCardTokenResults, GetMetaInfoParams,
GetMetaInfoResults, GetSpaceInfoParams, GetSpaceInfoResults, GetTokenListParams,
GetTokenListResults, UnbindParams, UnbindResults,
};
use api::user_capnp::user::{self, admin, card_d_e_s_fire_e_v2, info, manage};
use capnp::capability::Promise; use capnp::capability::Promise;
use capnp::Error;
use capnp_rpc::pry; use capnp_rpc::pry;
use std::borrow::Cow;
use std::io::Write;
use uuid::Uuid;
const TARGET: &str = "bffh::api::user";
#[derive(Clone)] #[derive(Clone)]
pub struct User { pub struct User {
span: tracing::Span,
session: SessionHandle, session: SessionHandle,
user: UserRef, user: UserRef,
} }
impl User { impl User {
pub fn new(session: SessionHandle, user: UserRef) -> Self { pub fn new(session: SessionHandle, user: UserRef) -> Self {
Self { session, user } let span = tracing::info_span!(target: TARGET, "User");
Self {
span,
session,
user,
}
} }
pub fn new_self(session: SessionHandle) -> Self { pub fn new_self(session: SessionHandle) -> Self {
@ -55,6 +73,7 @@ impl User {
} }
if session.has_perm(Permission::new("bffh.users.admin")) { if session.has_perm(Permission::new("bffh.users.admin")) {
builder.set_admin(capnp_rpc::new_client(client.clone())); builder.set_admin(capnp_rpc::new_client(client.clone()));
builder.set_card_d_e_s_fire_e_v2(capnp_rpc::new_client(client));
} }
} }
} }
@ -90,7 +109,7 @@ impl manage::Server for User {
if let Some(mut user) = self.session.users.get_user(uid) { if let Some(mut user) = self.session.users.get_user(uid) {
if let Ok(true) = user.check_password(old_pw.as_bytes()) { if let Ok(true) = user.check_password(old_pw.as_bytes()) {
user.set_pw(new_pw.as_bytes()); user.set_pw(new_pw.as_bytes());
self.session.users.put_user(uid, &user); pry!(self.session.users.put_user(uid, &user));
} }
} }
Promise::ok(()) Promise::ok(())
@ -124,9 +143,9 @@ impl admin::Server for User {
// Only update if needed // Only update if needed
if !target.userdata.roles.iter().any(|r| r.as_str() == rolename) { if !target.userdata.roles.iter().any(|r| r.as_str() == rolename) {
target.userdata.roles.push(rolename.to_string()); target.userdata.roles.push(rolename.to_string());
self.session pry!(self.session
.users .users
.put_user(self.user.get_username(), &target); .put_user(self.user.get_username(), &target));
} }
} }
@ -149,9 +168,9 @@ impl admin::Server for User {
// Only update if needed // Only update if needed
if target.userdata.roles.iter().any(|r| r.as_str() == rolename) { if target.userdata.roles.iter().any(|r| r.as_str() == rolename) {
target.userdata.roles.retain(|r| r.as_str() != rolename); target.userdata.roles.retain(|r| r.as_str() != rolename);
self.session pry!(self.session
.users .users
.put_user(self.user.get_username(), &target); .put_user(self.user.get_username(), &target));
} }
} }
@ -166,8 +185,218 @@ impl admin::Server for User {
let uid = self.user.get_username(); let uid = self.user.get_username();
if let Some(mut user) = self.session.users.get_user(uid) { if let Some(mut user) = self.session.users.get_user(uid) {
user.set_pw(new_pw.as_bytes()); user.set_pw(new_pw.as_bytes());
self.session.users.put_user(uid, &user); pry!(self.session.users.put_user(uid, &user));
} }
Promise::ok(()) Promise::ok(())
} }
} }
impl card_d_e_s_fire_e_v2::Server for User {
fn get_token_list(
&mut self,
_: GetTokenListParams,
mut results: GetTokenListResults,
) -> Promise<(), Error> {
let _guard = self.span.enter();
let _span = tracing::trace_span!(target: TARGET, "get_token_list").entered();
tracing::trace!("method call");
// TODO: This only supports a single token per user
let user = pry!(self
.session
.users
.get_user(self.user.get_username())
.ok_or_else(|| Error::failed(format!(
"User API object with nonexisting user \"{}\"",
self.user.get_username()
))));
let tk = user
.userdata
.kv
.get("cardtoken")
.map(|ck| hex::decode(ck).ok())
.flatten()
.unwrap_or_else(|| {
tracing::debug!(user.id = &user.id, "no tokens stored");
Vec::new()
});
if !tk.is_empty() {
let b = results.get();
let mut lb = b.init_token_list(1);
lb.set(0, &tk[..]);
}
Promise::ok(())
}
fn bind(&mut self, params: BindParams, _: BindResults) -> Promise<(), Error> {
let _guard = self.span.enter();
let _span = tracing::trace_span!(target: TARGET, "bind").entered();
let params = pry!(params.get());
let card_key = pry!(params.get_auth_key());
let token = pry!(params.get_token());
let token: Cow<'_, str> = if let Ok(url) = std::str::from_utf8(token) {
Cow::Borrowed(url)
} else {
Cow::Owned(hex::encode(token))
};
tracing::trace!(
params.token = token.as_ref(),
params.auth_key = "<censored>",
"method call"
);
let card_key = hex::encode(card_key);
let mut user = pry!(self
.session
.users
.get_user(self.user.get_username())
.ok_or_else(|| Error::failed(format!(
"User API object with nonexisting user \"{}\"",
self.user.get_username()
))));
let prev_token = user.userdata.kv.get("cardtoken");
let prev_cardk = user.userdata.kv.get("cardkey");
match (prev_token, prev_cardk) {
(Some(prev_token), Some(prev_cardk))
if prev_token.as_str() == &token && prev_cardk.as_str() == card_key.as_str() =>
{
tracing::info!(
user.id, token = token.as_ref(),
"new token and card key are identical, skipping no-op"
);
return Promise::ok(());
},
(Some(prev_token), Some(_))
if prev_token.as_str() == token /* above guard means prev_cardk != card_key */ =>
{
tracing::warn!(
token = token.as_ref(),
"trying to overwrite card key for existing token, ignoring!"
);
return Promise::ok(());
},
(Some(prev_token), None) => tracing::warn!(
user.id, prev_token,
"token already set for user but no card key, setting new pair unconditionally!"
),
(None, Some(_)) => tracing::warn!(
user.id,
"card key already set for user but no token, setting new pair unconditionally!"
),
(Some(_), Some(_)) | (None, None) => tracing::debug!(
user.id, token = token.as_ref(),
"Adding new card key/token pair"
),
}
user.userdata
.kv
.insert("cardtoken".to_string(), token.to_string());
user.userdata.kv.insert("cardkey".to_string(), card_key);
pry!(self.session.users.put_user(self.user.get_username(), &user));
Promise::ok(())
}
fn unbind(&mut self, params: UnbindParams, _: UnbindResults) -> Promise<(), Error> {
let _guard = self.span.enter();
let _span = tracing::trace_span!(target: TARGET, "unbind").entered();
let params = pry!(params.get());
let token = pry!(params.get_token());
let token: Cow<'_, str> = if let Ok(url) = std::str::from_utf8(token) {
Cow::Borrowed(url)
} else {
Cow::Owned(hex::encode(token))
};
tracing::trace!(params.token = token.as_ref(), "method call");
let mut user = pry!(self
.session
.users
.get_user(self.user.get_username())
.ok_or_else(|| Error::failed(format!(
"User API object with nonexisting user \"{}\"",
self.user.get_username()
))));
if let Some(prev_token) = user.userdata.kv.get("cardtoken") {
if token.as_ref() == prev_token.as_str() {
tracing::debug!(
user.id,
token = token.as_ref(),
"removing card key/token pair"
);
user.userdata.kv.remove("cardtoken");
user.userdata.kv.remove("cardkey");
}
}
pry!(self.session.users.put_user(self.user.get_username(), &user));
Promise::ok(())
}
fn gen_card_token(
&mut self,
_: GenCardTokenParams,
mut results: GenCardTokenResults,
) -> Promise<(), Error> {
let _guard = self.span.enter();
let _span = tracing::trace_span!(target: TARGET, "gen_card_token").entered();
tracing::trace!("method call");
results.get().set_token(Uuid::new_v4().as_bytes());
Promise::ok(())
}
fn get_meta_info(
&mut self,
_: GetMetaInfoParams,
mut results: GetMetaInfoResults,
) -> Promise<(), Error> {
let _guard = self.span.enter();
let _span = tracing::trace_span!(target: TARGET, "get_meta_info").entered();
tracing::trace!("method call");
results.get().set_bytes(b"FABACCESS\x00DESFIRE\x001.0\x00");
Promise::ok(())
}
fn get_space_info(
&mut self,
_: GetSpaceInfoParams,
mut results: GetSpaceInfoResults,
) -> Promise<(), Error> {
let _guard = self.span.enter();
let _span = tracing::trace_span!(target: TARGET, "get_space_info").entered();
tracing::trace!("method call");
let space = if let Some(space) = CONFIG.get().map(|c| c.spacename.as_str()) {
space
} else {
return Promise::err(Error::failed("No space name configured".to_string()));
};
let url = if let Some(url) = CONFIG.get().map(|c| c.instanceurl.as_str()) {
url
} else {
return Promise::err(Error::failed("No instance url configured".to_string()));
};
let mut data = Vec::new();
write!(&mut data, "urn:fabaccess:lab:{space}\x00{url}").unwrap();
results.get().set_bytes(&data);
Promise::ok(())
}
}

View File

@ -84,13 +84,13 @@ impl manage::Server for Users {
"method call" "method call"
); );
let mut builder = result.get(); let builder = result.get();
if !username.is_empty() && !password.is_empty() { if !username.is_empty() && !password.is_empty() {
if self.session.users.get_user(username).is_none() { if self.session.users.get_user(username).is_none() {
let user = db::User::new_with_plain_pw(username, password); let user = db::User::new_with_plain_pw(username, password);
self.session.users.put_user(username, &user); pry!(self.session.users.put_user(username, &user));
let mut builder = builder.init_successful(); let builder = builder.init_successful();
User::fill(&self.session, user, builder); User::fill(&self.session, user, builder);
} else { } else {
let mut builder = builder.init_failed(); let mut builder = builder.init_failed();

View File

@ -1,8 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::default::Default; use std::default::Default;
use std::error::Error; use std::fmt::Debug;
use std::fmt::{Debug, Display};
use std::marker::PhantomData;
use std::path::PathBuf; use std::path::PathBuf;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -12,7 +10,6 @@ use crate::authorization::roles::Role;
use crate::capnp::{Listen, TlsListen}; use crate::capnp::{Listen, TlsListen};
use crate::logging::LogConfig; use crate::logging::LogConfig;
use miette::IntoDiagnostic;
use std::path::Path; use std::path::Path;
#[derive(Debug)] #[derive(Debug)]
@ -96,6 +93,10 @@ pub struct Config {
#[serde(default, skip)] #[serde(default, skip)]
pub logging: LogConfig, pub logging: LogConfig,
pub spacename: String,
pub instanceurl: String,
} }
impl Config { impl Config {
@ -164,6 +165,8 @@ impl Default for Config {
tlskeylog: None, tlskeylog: None,
verbosity: 0, verbosity: 0,
logging: LogConfig::default(), logging: LogConfig::default(),
instanceurl: "".into(),
spacename: "".into(),
} }
} }
} }

View File

@ -38,13 +38,15 @@ pub fn read(file: impl AsRef<Path>) -> Result<Config, ConfigError> {
if !path.is_file() { if !path.is_file() {
return Err(ConfigError::NotAFile(path.to_string_lossy().to_string())); return Err(ConfigError::NotAFile(path.to_string_lossy().to_string()));
} }
let mut config = dhall::read_config_file(file)?; let config = dhall::read_config_file(file)?;
for (envvar, value) in std::env::vars() { // TODO: configuration by environment variables?
match envvar.as_str() { // but rather in in a separate function
// Do things like this? // for (envvar, value) in std::env::vars() {
// "BFFH_LOG" => config.logging.filter = Some(value), // match envvar.as_str() {
_ => {} // // Do things like this?
} // // "BFFH_LOG" => config.logging.filter = Some(value),
} // _ => {}
// }
// }
Ok(config) Ok(config)
} }

View File

@ -1,10 +1,13 @@
use thiserror::Error; use thiserror::Error;
// for converting a database error into a failed promise
use capnp;
mod raw; mod raw;
use miette::{Diagnostic, LabeledSpan, Severity, SourceCode}; use miette::{Diagnostic, Severity};
pub use raw::RawDB; pub use raw::RawDB;
use std::fmt::{Debug, Display, Formatter}; use std::fmt::{Debug, Display};
mod typed; mod typed;
pub use typed::{Adapter, AlignedAdapter, ArchivedValue, DB}; pub use typed::{Adapter, AlignedAdapter, ArchivedValue, DB};
@ -13,9 +16,9 @@ pub type ErrorO = lmdb::Error;
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
#[repr(transparent)] #[derive(Clone, Debug, PartialEq, Eq, Error)]
#[derive(Debug, Error)]
#[error(transparent)] #[error(transparent)]
#[repr(transparent)]
pub struct Error(#[from] lmdb::Error); pub struct Error(#[from] lmdb::Error);
impl Diagnostic for Error { impl Diagnostic for Error {
@ -79,3 +82,9 @@ impl Diagnostic for Error {
None None
} }
} }
impl From<Error> for capnp::Error {
fn from(dberr: Error) -> capnp::Error {
capnp::Error::failed(format!("database error: {}", dberr.to_string()))
}
}

View File

@ -1,4 +1,3 @@
use super::Result;
use lmdb::{DatabaseFlags, Environment, RwTransaction, Transaction, WriteFlags}; use lmdb::{DatabaseFlags, Environment, RwTransaction, Transaction, WriteFlags};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@ -1,4 +1,4 @@
use miette::{Diagnostic, LabeledSpan, Severity, SourceCode}; use miette::{Diagnostic, Severity};
use std::error; use std::error;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::io; use std::io;

View File

@ -5,14 +5,11 @@ use super::Initiator;
use crate::initiators::InitiatorCallbacks; use crate::initiators::InitiatorCallbacks;
use crate::resources::modules::fabaccess::Status; use crate::resources::modules::fabaccess::Status;
use crate::session::SessionHandle; use crate::session::SessionHandle;
use crate::users::UserRef;
use async_io::Timer; use async_io::Timer;
use futures_util::future::BoxFuture; use futures_util::future::BoxFuture;
use futures_util::ready; use futures_util::ready;
use lmdb::Stat;
use std::collections::HashMap; use std::collections::HashMap;
use std::future::Future; use std::future::Future;
use std::mem;
use std::pin::Pin; use std::pin::Pin;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@ -64,10 +61,7 @@ impl Future for Dummy {
match &mut self.state { match &mut self.state {
DummyState::Empty => { DummyState::Empty => {
tracing::trace!("Dummy initiator is empty, initializing…"); tracing::trace!("Dummy initiator is empty, initializing…");
mem::replace( self.state = DummyState::Sleeping(Self::timer(), Some(Status::Free));
&mut self.state,
DummyState::Sleeping(Self::timer(), Some(Status::Free)),
);
} }
DummyState::Sleeping(timer, next) => { DummyState::Sleeping(timer, next) => {
tracing::trace!("Sleep timer exists, polling it."); tracing::trace!("Sleep timer exists, polling it.");
@ -78,7 +72,7 @@ impl Future for Dummy {
let status = next.take().unwrap(); let status = next.take().unwrap();
let f = self.flip(status); let f = self.flip(status);
mem::replace(&mut self.state, DummyState::Updating(f)); self.state = DummyState::Updating(f);
} }
DummyState::Updating(f) => { DummyState::Updating(f) => {
tracing::trace!("Update future exists, polling it ."); tracing::trace!("Update future exists, polling it .");
@ -87,10 +81,7 @@ impl Future for Dummy {
tracing::trace!("Update future completed, sleeping!"); tracing::trace!("Update future completed, sleeping!");
mem::replace( self.state = DummyState::Sleeping(Self::timer(), Some(next));
&mut self.state,
DummyState::Sleeping(Self::timer(), Some(next)),
);
} }
} }
} }

View File

@ -3,22 +3,15 @@ use crate::initiators::process::Process;
use crate::resources::modules::fabaccess::Status; use crate::resources::modules::fabaccess::Status;
use crate::session::SessionHandle; use crate::session::SessionHandle;
use crate::{ use crate::{
AuthenticationHandle, Config, MachineState, Resource, ResourcesHandle, SessionManager, AuthenticationHandle, Config, Resource, ResourcesHandle, SessionManager,
}; };
use async_compat::CompatExt;
use executor::prelude::Executor; use executor::prelude::Executor;
use futures_util::ready; use futures_util::ready;
use miette::IntoDiagnostic;
use rumqttc::ConnectReturnCode::Success;
use rumqttc::{AsyncClient, ConnectionError, Event, Incoming, MqttOptions};
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Display;
use std::future::Future; use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use std::time::Duration;
use tracing::Span; use tracing::Span;
use url::Url;
mod dummy; mod dummy;
mod process; mod process;
@ -56,7 +49,7 @@ impl InitiatorCallbacks {
} }
pub fn open_session(&self, uid: &str) -> Option<SessionHandle> { pub fn open_session(&self, uid: &str) -> Option<SessionHandle> {
self.sessions.open(&self.span, uid) self.sessions.try_open(&self.span, uid)
} }
} }
@ -107,7 +100,7 @@ pub fn load(
config: &Config, config: &Config,
resources: ResourcesHandle, resources: ResourcesHandle,
sessions: SessionManager, sessions: SessionManager,
authentication: AuthenticationHandle, _authentication: AuthenticationHandle,
) -> miette::Result<()> { ) -> miette::Result<()> {
let span = tracing::info_span!("loading initiators"); let span = tracing::info_span!("loading initiators");
let _guard = span.enter(); let _guard = span.enter();

View File

@ -1,9 +1,9 @@
use super::Initiator; use super::Initiator;
use super::InitiatorCallbacks; use super::InitiatorCallbacks;
use crate::resources::state::State; use crate::resources::modules::fabaccess::Status;
use crate::utils::linebuffer::LineBuffer; use crate::utils::linebuffer::LineBuffer;
use async_process::{Child, ChildStderr, ChildStdout, Command, Stdio}; use async_process::{Child, ChildStderr, ChildStdout, Command, Stdio};
use futures_lite::{ready, AsyncRead}; use futures_lite::AsyncRead;
use miette::{miette, IntoDiagnostic}; use miette::{miette, IntoDiagnostic};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
@ -11,7 +11,6 @@ use std::future::Future;
use std::io; use std::io;
use std::pin::Pin; use std::pin::Pin;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use crate::resources::modules::fabaccess::Status;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum InputMessage { pub enum InputMessage {
@ -63,7 +62,12 @@ struct ProcessState {
impl ProcessState { impl ProcessState {
pub fn new(stdout: ChildStdout, stderr: ChildStderr, child: Child) -> Self { pub fn new(stdout: ChildStdout, stderr: ChildStderr, child: Child) -> Self {
Self { stdout, stderr, stderr_closed: false, child } Self {
stdout,
stderr,
stderr_closed: false,
child,
}
} }
fn try_process(&mut self, buffer: &[u8], callbacks: &mut InitiatorCallbacks) -> usize { fn try_process(&mut self, buffer: &[u8], callbacks: &mut InitiatorCallbacks) -> usize {
@ -100,7 +104,9 @@ impl ProcessState {
let InputMessage::SetState(status) = state; let InputMessage::SetState(status) = state;
callbacks.set_status(status); callbacks.set_status(status);
} }
Err(error) => tracing::warn!(%error, "process initiator did not send a valid line"), Err(error) => {
tracing::warn!(%error, "process initiator did not send a valid line")
}
} }
} }
} }
@ -110,7 +116,7 @@ impl ProcessState {
impl Future for Process { impl Future for Process {
type Output = (); type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if let Process { if let Process {
state: Some(state), state: Some(state),
buffer, buffer,
@ -202,8 +208,8 @@ impl Future for Process {
impl Initiator for Process { impl Initiator for Process {
fn new(params: &HashMap<String, String>, callbacks: InitiatorCallbacks) -> miette::Result<Self> fn new(params: &HashMap<String, String>, callbacks: InitiatorCallbacks) -> miette::Result<Self>
where where
Self: Sized, Self: Sized,
{ {
let cmd = params let cmd = params
.get("cmd") .get("cmd")

View File

@ -3,11 +3,14 @@
//#![warn(missing_docs)] //#![warn(missing_docs)]
//#![warn(missing_crate_level_docs)] //#![warn(missing_crate_level_docs)]
//! Diflouroborane //! Difluoroborane
//! //!
//! This is the capnp component of the FabAccess project. //! This is the capnp component of the FabAccess project.
//! The entry point of bffhd can be found in [bin/bffhd/main.rs](../bin/bffhd/main.rs) //! The entry point of bffhd can be found in [bin/bffhd/main.rs](../bin/bffhd/main.rs)
use miette::{Diagnostic, IntoDiagnostic};
use thiserror::Error;
pub mod config; pub mod config;
/// Internal Databases build on top of LMDB, a mmap()'ed B-tree DB optimized for reads /// Internal Databases build on top of LMDB, a mmap()'ed B-tree DB optimized for reads
@ -44,7 +47,6 @@ mod tls;
use std::sync::Arc; use std::sync::Arc;
use futures_util::{FutureExt, StreamExt}; use futures_util::{FutureExt, StreamExt};
use miette::{Context, IntoDiagnostic, Report};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use crate::audit::AuditLog; use crate::audit::AuditLog;
@ -65,7 +67,9 @@ use lightproc::recoverable_handle::RecoverableHandle;
use signal_hook::consts::signal::*; use signal_hook::consts::signal::*;
use tracing::Span; use tracing::Span;
pub struct Diflouroborane { use std::collections::HashMap;
pub struct Difluoroborane {
config: Config, config: Config,
executor: Executor<'static>, executor: Executor<'static>,
pub statedb: StateDB, pub statedb: StateDB,
@ -77,15 +81,72 @@ pub struct Diflouroborane {
pub static RESOURCES: OnceCell<ResourcesHandle> = OnceCell::new(); pub static RESOURCES: OnceCell<ResourcesHandle> = OnceCell::new();
pub static CONFIG: OnceCell<Config> = OnceCell::new();
struct SignalHandlerErr; struct SignalHandlerErr;
impl error::Description for SignalHandlerErr { impl error::Description for SignalHandlerErr {
const CODE: &'static str = "signals::new"; const CODE: &'static str = "signals::new";
} }
impl Diflouroborane { #[derive(Debug, Error, Diagnostic)]
// TODO 0.5: #[non_exhaustive]
pub enum BFFHError {
#[error("DB operation failed")]
DBError(
#[from]
#[source]
db::Error,
),
#[error("failed to initialize global user store")]
UsersError(
#[from]
#[source]
users::Error,
),
#[error("failed to initialize state database")]
StateDBError(
#[from]
#[source]
resources::state::db::StateDBError,
),
#[error("audit log failed")]
AuditLogError(
#[from]
#[source]
audit::Error,
),
#[error("Failed to initialize signal handler")]
SignalsError(#[source] std::io::Error),
#[error("error in actor subsystem")]
ActorError(
#[from]
#[source]
actors::ActorError,
),
#[error("failed to initialize TLS config")]
TlsSetup(
#[from]
#[source]
tls::Error,
),
#[error("API handler failed")]
ApiError(
#[from]
#[source]
capnp::Error,
),
}
#[derive(serde::Serialize, serde::Deserialize)]
struct DatabaseDump {
users: HashMap<String, users::db::UserData>,
state: HashMap<String, resources::state::State>,
}
impl Difluoroborane {
pub fn setup() {} pub fn setup() {}
pub fn new(config: Config) -> miette::Result<Self> { pub fn new(config: Config) -> Result<Self, BFFHError> {
let mut server = logging::init(&config.logging); let mut server = logging::init(&config.logging);
let span = tracing::info_span!( let span = tracing::info_span!(
target: "bffh", target: "bffh",
@ -121,9 +182,7 @@ impl Diflouroborane {
let users = Users::new(env.clone())?; let users = Users::new(env.clone())?;
let roles = Roles::new(config.roles.clone()); let roles = Roles::new(config.roles.clone());
let _audit_log = AuditLog::new(&config) let _audit_log = AuditLog::new(&config)?;
.into_diagnostic()
.wrap_err("Failed to initialize audit log")?;
let resources = ResourcesHandle::new(config.machines.iter().map(|(id, desc)| { let resources = ResourcesHandle::new(config.machines.iter().map(|(id, desc)| {
Resource::new(Arc::new(resources::Inner::new( Resource::new(Arc::new(resources::Inner::new(
@ -132,7 +191,8 @@ impl Diflouroborane {
desc.clone(), desc.clone(),
))) )))
})); }));
RESOURCES.set(resources.clone()); RESOURCES.set(resources.clone()).unwrap();
CONFIG.set(config.clone()).unwrap();
Ok(Self { Ok(Self {
config, config,
@ -145,10 +205,27 @@ impl Diflouroborane {
}) })
} }
pub fn run(&mut self) -> miette::Result<()> { pub fn dump_db(&mut self, file: &str) -> Result<(), miette::Error> {
let users = self.users.dump_map()?;
let state = self.statedb.dump_map()?;
let dump = DatabaseDump{users, state};
let data = toml::ser::to_vec(&dump).map_err(|e| miette::Error::msg(format!("Serializing database dump failed: {}", e)))?;
std::fs::write(file, &data).map_err(|e| miette::Error::msg(format!("writing database dump failed: {}", e)))?;
Ok(())
}
pub fn load_db(&mut self, file: &str) -> Result<(), miette::Error> {
let data = std::fs::read(file).into_diagnostic()?;
let dump: DatabaseDump = toml::de::from_slice(&data).into_diagnostic()?;
self.users.load_map(&dump.users)?;
self.statedb.load_map(&dump.state)?;
Ok(())
}
pub fn run(&mut self) -> Result<(), BFFHError> {
let _guard = self.span.enter(); let _guard = self.span.enter();
let mut signals = signal_hook_async_std::Signals::new(&[SIGINT, SIGQUIT, SIGTERM]) let mut signals = signal_hook_async_std::Signals::new(&[SIGINT, SIGQUIT, SIGTERM])
.map_err(|ioerr| error::wrap::<SignalHandlerErr>(ioerr.into()))?; .map_err(BFFHError::SignalsError)?;
let sessionmanager = SessionManager::new(self.users.clone(), self.roles.clone()); let sessionmanager = SessionManager::new(self.users.clone(), self.roles.clone());
let authentication = AuthenticationHandle::new(self.users.clone()); let authentication = AuthenticationHandle::new(self.users.clone());
@ -159,11 +236,12 @@ impl Diflouroborane {
self.resources.clone(), self.resources.clone(),
sessionmanager.clone(), sessionmanager.clone(),
authentication.clone(), authentication.clone(),
); ).expect("initializing initiators failed");
// TODO 0.5: error handling. Add variant to BFFHError
actors::load(self.executor.clone(), &self.config, self.resources.clone())?; actors::load(self.executor.clone(), &self.config, self.resources.clone())?;
let tlsconfig = TlsConfig::new(self.config.tlskeylog.as_ref(), !self.config.is_quiet()) let tlsconfig = TlsConfig::new(self.config.tlskeylog.as_ref(), !self.config.is_quiet())?;
.into_diagnostic()?;
let acceptor = tlsconfig.make_tls_acceptor(&self.config.tlsconfig)?; let acceptor = tlsconfig.make_tls_acceptor(&self.config.tlsconfig)?;
let apiserver = self.executor.run(APIServer::bind( let apiserver = self.executor.run(APIServer::bind(
@ -179,13 +257,13 @@ impl Diflouroborane {
self.executor.spawn(apiserver.handle_until(rx)); self.executor.spawn(apiserver.handle_until(rx));
let f = async { let f = async {
let mut sig = None; let mut sig;
while { while {
sig = signals.next().await; sig = signals.next().await;
sig.is_none() sig.is_none()
} {} } {}
tracing::info!(signal = %sig.unwrap(), "Received signal"); tracing::info!(signal = %sig.unwrap(), "Received signal");
tx.send(()); _ = tx.send(()); // ignore result, as an Err means that the executor we want to stop has already stopped
}; };
self.executor.run(f); self.executor.run(f);

View File

@ -2,16 +2,15 @@ use serde::{Deserialize, Serialize};
use std::path::Path; use std::path::Path;
use tracing_subscriber::fmt::format::Format; use tracing_subscriber::fmt::format::Format;
use tracing_subscriber::prelude::*; use tracing_subscriber::prelude::*;
use tracing_subscriber::reload::Handle; use tracing_subscriber::EnvFilter;
use tracing_subscriber::{reload, EnvFilter};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogConfig { pub struct LogConfig {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
/// Log filter string in the tracing format `target[span{field=value}]=level`. /// Log filter string in the tracing format `target[span{field=value}]=level`.
/// lvalue is optional and multiple filters can be combined with comma. /// lvalue is optional and multiple filters can be combined with comma.
/// e.g. `warn,diflouroborane::actors=debug` will only print `WARN` and `ERROR` unless the /// e.g. `warn,difluoroborane::actors=debug` will only print `WARN` and `ERROR` unless the
/// message is logged in a span below `diflouroborane::actors` (i.e. by an actor task) in /// message is logged in a span below `difluoroborane::actors` (i.e. by an actor task) in
/// which case `DEBUG` and `INFO` will also be printed. /// which case `DEBUG` and `INFO` will also be printed.
pub filter: Option<String>, pub filter: Option<String>,

View File

@ -85,10 +85,13 @@ impl Inner {
self.db.put(&self.id.as_bytes(), &state).unwrap(); self.db.put(&self.id.as_bytes(), &state).unwrap();
tracing::trace!("Updated DB, sending update signal"); tracing::trace!("Updated DB, sending update signal");
AUDIT let res = AUDIT
.get() .get()
.unwrap() .unwrap()
.log(self.id.as_str(), &format!("{}", state)); .log(self.id.as_str(), &format!("{}", state));
if let Err(e) = res {
tracing::error!("Writing to the audit log failed for {} {}: {e}", self.id.as_str(), state);
}
self.signal.set(state); self.signal.set(state);
tracing::trace!("Sent update signal"); tracing::trace!("Sent update signal");
@ -161,7 +164,7 @@ impl Resource {
fn set_state(&self, state: MachineState) { fn set_state(&self, state: MachineState) {
let mut serializer = AllocSerializer::<1024>::default(); let mut serializer = AllocSerializer::<1024>::default();
serializer.serialize_value(&state); serializer.serialize_value(&state).expect("serializing a MachineState shoud be infallible");
let archived = ArchivedValue::new(serializer.into_serializer().into_inner()); let archived = ArchivedValue::new(serializer.into_serializer().into_inner());
self.inner.set_state(archived) self.inner.set_state(archived)
} }

View File

@ -3,7 +3,6 @@ use crate::utils::oid::ObjectIdentifier;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use rkyv::{Archive, Archived, Deserialize, Infallible}; use rkyv::{Archive, Archived, Deserialize, Infallible};
use std::fmt; use std::fmt;
use std::fmt::Write;
use std::str::FromStr; use std::str::FromStr;
//use crate::oidvalue; //use crate::oidvalue;

View File

@ -2,6 +2,7 @@ use crate::resources::Resource;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
#[derive(Debug)]
struct Inner { struct Inner {
id: HashMap<String, Resource>, id: HashMap<String, Resource>,
} }
@ -19,7 +20,7 @@ impl Inner {
} }
} }
#[derive(Clone)] #[derive(Clone, Debug)]
pub struct ResourcesHandle { pub struct ResourcesHandle {
inner: Arc<Inner>, inner: Arc<Inner>,
} }

View File

@ -1,12 +1,12 @@
use rkyv::ser::Serializer;
use rkyv::ser::serializers::AllocSerializer;
use thiserror::Error; use thiserror::Error;
use crate::db; use crate::db;
use crate::db::{AlignedAdapter, ArchivedValue, RawDB, DB}; use crate::db::{AlignedAdapter, ArchivedValue, RawDB, DB};
use lmdb::{DatabaseFlags, Environment, EnvironmentFlags, Transaction, WriteFlags}; use lmdb::{DatabaseFlags, Environment, EnvironmentFlags, Transaction, WriteFlags};
use miette::{Diagnostic, LabeledSpan, Severity, SourceCode}; use miette::Diagnostic;
use std::any::TypeId; use std::fmt::Debug;
use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
use std::{path::Path, sync::Arc}; use std::{path::Path, sync::Arc};
use crate::resources::state::State; use crate::resources::state::State;
@ -17,7 +17,7 @@ pub struct StateDB {
db: DB<AlignedAdapter<State>>, db: DB<AlignedAdapter<State>>,
} }
#[derive(Debug, Error, Diagnostic)] #[derive(Clone, Debug, PartialEq, Eq, Error, Diagnostic)]
pub enum StateDBError { pub enum StateDBError {
#[error("opening the state db environment failed")] #[error("opening the state db environment failed")]
#[diagnostic( #[diagnostic(
@ -54,8 +54,8 @@ impl StateDB {
} }
pub fn open_with_env(env: Arc<Environment>) -> Result<Self, StateDBError> { pub fn open_with_env(env: Arc<Environment>) -> Result<Self, StateDBError> {
let db = unsafe { RawDB::open(&env, Some("state")) }; let db = RawDB::open(&env, Some("state"))
let db = db.map_err(|e| StateDBError::Open(e.into()))?; .map_err(|e| StateDBError::Open(e.into()))?;
Ok(Self::new(env, db)) Ok(Self::new(env, db))
} }
@ -66,8 +66,8 @@ impl StateDB {
pub fn create_with_env(env: Arc<Environment>) -> Result<Self, StateDBError> { pub fn create_with_env(env: Arc<Environment>) -> Result<Self, StateDBError> {
let flags = DatabaseFlags::empty(); let flags = DatabaseFlags::empty();
let db = unsafe { RawDB::create(&env, Some("state"), flags) }; let db = RawDB::create(&env, Some("state"), flags)
let db = db.map_err(|e| StateDBError::Create(e.into()))?; .map_err(|e| StateDBError::Create(e.into()))?;
Ok(Self::new(env, db)) Ok(Self::new(env, db))
} }
@ -99,6 +99,30 @@ impl StateDB {
self.db.put(&mut txn, key, val, flags)?; self.db.put(&mut txn, key, val, flags)?;
Ok(txn.commit()?) Ok(txn.commit()?)
} }
pub fn load_map(&self, map: &std::collections::HashMap<String, State>) -> miette::Result<()> {
use miette::IntoDiagnostic;
let mut txn = self.env.begin_rw_txn().into_diagnostic()?;
let flags = WriteFlags::empty();
for (key, val) in map {
let mut serializer = AllocSerializer::<1024>::default();
serializer.serialize_value(val).into_diagnostic()?;
let serialized = ArchivedValue::new(serializer.into_serializer().into_inner());
self.db.put(&mut txn, &key.as_bytes(), &serialized, flags)?;
}
txn.commit().into_diagnostic()?;
Ok(())
}
pub fn dump_map(&self) -> miette::Result<std::collections::HashMap<String, State>> {
let mut map = std::collections::HashMap::new();
for (key, val) in self.get_all(&self.begin_ro_txn()?)? {
let key_str = core::str::from_utf8(&key).map_err(|_e| miette::Error::msg("state key not UTF8"))?.to_string();
let val_state: State = rkyv::Deserialize::deserialize(val.as_ref(), &mut rkyv::Infallible).unwrap();
map.insert(key_str, val_state);
}
Ok(map)
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1,5 +1,5 @@
use std::fmt::{Debug, Display, Formatter}; use std::fmt::{Debug, Display, Formatter};
use std::{fmt, hash::Hasher}; use std::fmt;
use std::ops::Deref; use std::ops::Deref;

View File

@ -14,8 +14,6 @@ use inventory;
use rkyv::ser::{ScratchSpace, Serializer}; use rkyv::ser::{ScratchSpace, Serializer};
use serde::ser::SerializeMap;
use std::collections::HashMap; use std::collections::HashMap;
use std::ops::Deref; use std::ops::Deref;
@ -275,10 +273,6 @@ pub struct ImplDebugInfo {
/// [statevalue_register](macro@crate::statevalue_register) macro with your OID as first and type /// [statevalue_register](macro@crate::statevalue_register) macro with your OID as first and type
/// as second parameter like so: /// as second parameter like so:
/// ///
/// ```no_run
/// struct MyStruct;
/// statevalue_register!(ObjectIdentifier::from_str("1.3.6.1.4.1.48398.612.1.14").unwrap(), MyStruct)
/// ```
pub struct ImplEntry<'a> { pub struct ImplEntry<'a> {
id: ImplId<'a>, id: ImplId<'a>,
data: ImplData<'a>, data: ImplData<'a>,

View File

@ -1,6 +1,7 @@
use crate::authorization::permissions::Permission; use crate::authorization::permissions::Permission;
use crate::authorization::roles::Roles; use crate::authorization::roles::Roles;
use crate::resources::Resource; use crate::resources::Resource;
use crate::users::db::User;
use crate::users::{db, UserRef}; use crate::users::{db, UserRef};
use crate::Users; use crate::Users;
use tracing::Span; use tracing::Span;
@ -16,25 +17,27 @@ impl SessionManager {
Self { users, roles } Self { users, roles }
} }
pub fn try_open(&self, parent: &Span, uid: impl AsRef<str>) -> Option<SessionHandle> {
self.users
.get_user(uid.as_ref())
.map(|user| self.open(parent, user))
}
// TODO: make infallible // TODO: make infallible
pub fn open(&self, parent: &Span, uid: impl AsRef<str>) -> Option<SessionHandle> { pub fn open(&self, parent: &Span, user: User) -> SessionHandle {
let uid = uid.as_ref(); let uid = user.id.as_str();
if let Some(user) = self.users.get_user(uid) { let span = tracing::info_span!(
let span = tracing::info_span!( target: "bffh::api",
target: "bffh::api", parent: parent,
parent: parent, "session",
"session", uid,
uid = uid, );
); tracing::trace!(parent: &span, uid, ?user, "opening session");
tracing::trace!(parent: &span, uid, ?user, "opening session"); SessionHandle {
Some(SessionHandle { span,
span, users: self.users.clone(),
users: self.users.clone(), roles: self.roles.clone(),
roles: self.roles.clone(), user: UserRef::new(user.id),
user: UserRef::new(user.id),
})
} else {
None
} }
} }
} }

View File

@ -1,17 +1,19 @@
use std::fs::File; use std::fs::File;
use std::io; use std::io;
use std::io::BufReader; use std::io::BufReader;
use std::path::Path; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use crate::capnp::TlsListen; use crate::capnp::TlsListen;
use futures_rustls::TlsAcceptor; use futures_rustls::TlsAcceptor;
use miette::IntoDiagnostic; use miette::Diagnostic;
use rustls::version::{TLS12, TLS13}; use rustls::version::{TLS12, TLS13};
use rustls::{Certificate, PrivateKey, ServerConfig, SupportedCipherSuite}; use rustls::{Certificate, PrivateKey, ServerConfig, SupportedCipherSuite};
use thiserror::Error;
use tracing::Level; use tracing::Level;
use crate::keylog::KeyLogFile; use crate::keylog::KeyLogFile;
use crate::tls::Error::KeyLogOpen;
fn lookup_cipher_suite(name: &str) -> Option<SupportedCipherSuite> { fn lookup_cipher_suite(name: &str) -> Option<SupportedCipherSuite> {
match name { match name {
@ -47,8 +49,32 @@ pub struct TlsConfig {
keylog: Option<Arc<KeyLogFile>>, keylog: Option<Arc<KeyLogFile>>,
} }
#[derive(Debug, Error, Diagnostic)]
pub enum Error {
#[error("failed to open certificate file at path {0}")]
OpenCertFile(PathBuf, #[source] io::Error),
#[error("failed to open private key file at path {0}")]
OpenKeyFile(PathBuf, #[source] io::Error),
#[error("failed to read system certs")]
SystemCertsFile(#[source] io::Error),
#[error("failed to read from key file")]
ReadKeyFile(#[source] io::Error),
#[error("private key file must contain a single PEM-encoded private key")]
KeyFileFormat,
#[error("invalid TLS version {0}")]
TlsVersion(String),
#[error("Initializing TLS context failed")]
Builder(
#[from]
#[source]
rustls::Error,
),
#[error("failed to initialize key log")]
KeyLogOpen(#[source] io::Error),
}
impl TlsConfig { impl TlsConfig {
pub fn new(keylogfile: Option<impl AsRef<Path>>, warn: bool) -> io::Result<Self> { pub fn new(keylogfile: Option<impl AsRef<Path>>, warn: bool) -> Result<Self, Error> {
let span = tracing::span!(Level::INFO, "tls"); let span = tracing::span!(Level::INFO, "tls");
let _guard = span.enter(); let _guard = span.enter();
@ -57,7 +83,11 @@ impl TlsConfig {
} }
if let Some(path) = keylogfile { if let Some(path) = keylogfile {
let keylog = Some(KeyLogFile::new(path).map(|ok| Arc::new(ok))?); let keylog = Some(
KeyLogFile::new(path)
.map(|ok| Arc::new(ok))
.map_err(KeyLogOpen)?,
);
Ok(Self { keylog }) Ok(Self { keylog })
} else { } else {
Ok(Self { keylog: None }) Ok(Self { keylog: None })
@ -75,27 +105,31 @@ impl TlsConfig {
} }
} }
pub fn make_tls_acceptor(&self, config: &TlsListen) -> miette::Result<TlsAcceptor> { pub fn make_tls_acceptor(&self, config: &TlsListen) -> Result<TlsAcceptor, Error> {
let span = tracing::debug_span!("tls"); let span = tracing::debug_span!("tls");
let _guard = span.enter(); let _guard = span.enter();
tracing::debug!(path = %config.certfile.as_path().display(), "reading certificates"); let path = config.certfile.as_path();
let mut certfp = BufReader::new(File::open(config.certfile.as_path()).into_diagnostic()?); tracing::debug!(path = %path.display(), "reading certificates");
let mut certfp =
BufReader::new(File::open(path).map_err(|e| Error::OpenCertFile(path.into(), e))?);
let certs = rustls_pemfile::certs(&mut certfp) let certs = rustls_pemfile::certs(&mut certfp)
.into_diagnostic()? .map_err(Error::SystemCertsFile)?
.into_iter() .into_iter()
.map(Certificate) .map(Certificate)
.collect(); .collect();
tracing::debug!(path = %config.keyfile.as_path().display(), "reading private key"); let path = config.keyfile.as_path();
let mut keyfp = BufReader::new(File::open(config.keyfile.as_path()).into_diagnostic()?); tracing::debug!(path = %path.display(), "reading private key");
let key = match rustls_pemfile::read_one(&mut keyfp).into_diagnostic()? { let mut keyfp =
BufReader::new(File::open(path).map_err(|err| Error::OpenKeyFile(path.into(), err))?);
let key = match rustls_pemfile::read_one(&mut keyfp).map_err(Error::ReadKeyFile)? {
Some(rustls_pemfile::Item::PKCS8Key(key) | rustls_pemfile::Item::RSAKey(key)) => { Some(rustls_pemfile::Item::PKCS8Key(key) | rustls_pemfile::Item::RSAKey(key)) => {
PrivateKey(key) PrivateKey(key)
} }
_ => { _ => {
tracing::error!("private key file invalid"); tracing::error!("private key file invalid");
miette::bail!("private key file must contain a PEM-encoded private key") return Err(Error::KeyFileFormat);
} }
}; };
@ -104,20 +138,19 @@ impl TlsConfig {
.with_safe_default_kx_groups(); .with_safe_default_kx_groups();
let tls_builder = if let Some(ref min) = config.tls_min_version { let tls_builder = if let Some(ref min) = config.tls_min_version {
match min.as_str() { let v = min.to_lowercase();
match v.as_str() {
"tls12" => tls_builder.with_protocol_versions(&[&TLS12]), "tls12" => tls_builder.with_protocol_versions(&[&TLS12]),
"tls13" => tls_builder.with_protocol_versions(&[&TLS13]), "tls13" => tls_builder.with_protocol_versions(&[&TLS13]),
x => miette::bail!("TLS version {} is invalid", x), _ => return Err(Error::TlsVersion(v)),
} }
} else { } else {
tls_builder.with_safe_default_protocol_versions() tls_builder.with_safe_default_protocol_versions()
} }?;
.into_diagnostic()?;
let mut tls_config = tls_builder let mut tls_config = tls_builder
.with_no_client_auth() .with_no_client_auth()
.with_single_cert(certs, key) .with_single_cert(certs, key)?;
.into_diagnostic()?;
if let Some(keylog) = &self.keylog { if let Some(keylog) = &self.keylog {
tls_config.key_log = keylog.clone(); tls_config.key_log = keylog.clone();

View File

@ -2,7 +2,6 @@ use lmdb::{DatabaseFlags, Environment, RwTransaction, Transaction, WriteFlags};
use rkyv::Infallible; use rkyv::Infallible;
use std::collections::HashMap; use std::collections::HashMap;
use miette::{Context, IntoDiagnostic};
use std::sync::Arc; use std::sync::Arc;
use crate::db; use crate::db;
@ -11,6 +10,8 @@ use rkyv::ser::serializers::AllocSerializer;
use rkyv::ser::Serializer; use rkyv::ser::Serializer;
use rkyv::Deserialize; use rkyv::Deserialize;
pub use crate::db::Error;
#[derive( #[derive(
Clone, Clone,
PartialEq, PartialEq,
@ -34,11 +35,9 @@ fn hash_pw(pw: &[u8]) -> argon2::Result<String> {
} }
impl User { impl User {
pub fn check_password(&self, pwd: &[u8]) -> miette::Result<bool> { pub fn check_password(&self, pwd: &[u8]) -> Result<bool, argon2::Error> {
if let Some(ref encoded) = self.userdata.passwd { if let Some(ref encoded) = self.userdata.passwd {
argon2::verify_encoded(encoded, pwd) argon2::verify_encoded(encoded, pwd)
.into_diagnostic()
.wrap_err("Stored password is an invalid string")
} else { } else {
Ok(false) Ok(false)
} }
@ -183,8 +182,8 @@ impl UserDB {
} }
pub fn clear_txn(&self, txn: &mut RwTransaction) -> Result<(), db::Error> { pub fn clear_txn(&self, txn: &mut RwTransaction) -> Result<(), db::Error> {
self.db.clear(txn); // TODO: why was the result ignored here?
Ok(()) self.db.clear(txn)
} }
pub fn get_all(&self) -> Result<HashMap<String, UserData>, db::Error> { pub fn get_all(&self) -> Result<HashMap<String, UserData>, db::Error> {

View File

@ -7,10 +7,10 @@ use std::collections::HashMap;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::io::Write; use std::io::Write;
use clap::ArgMatches; use miette::{Diagnostic, IntoDiagnostic, SourceSpan};
use miette::{Context, Diagnostic, IntoDiagnostic, SourceOffset, SourceSpan};
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use thiserror::Error; use thiserror::Error;
pub mod db; pub mod db;
@ -69,17 +69,20 @@ pub struct Users {
userdb: &'static UserDB, userdb: &'static UserDB,
} }
#[derive(Clone, Debug, PartialEq, Eq, Error, Diagnostic)]
#[error(transparent)]
#[repr(transparent)]
pub struct Error(#[from] pub db::Error);
impl Users { impl Users {
pub fn new(env: Arc<Environment>) -> miette::Result<Self> { pub fn new(env: Arc<Environment>) -> Result<Self, Error> {
let span = tracing::debug_span!("users", ?env, "Creating Users handle"); let span = tracing::debug_span!("users", ?env, "Creating Users handle");
let _guard = span.enter(); let _guard = span.enter();
let userdb = USERDB let userdb = USERDB.get_or_try_init(|| {
.get_or_try_init(|| { tracing::debug!("Global resource not yet initialized, initializing…");
tracing::debug!("Global resource not yet initialized, initializing…"); unsafe { UserDB::create(env) }
unsafe { UserDB::create(env) } })?;
})
.wrap_err("Failed to open userdb")?;
Ok(Self { userdb }) Ok(Self { userdb })
} }
@ -170,6 +173,29 @@ impl Users {
Ok(()) Ok(())
} }
pub fn load_map(&mut self, dump: &HashMap<String,UserData>) -> miette::Result<()> {
let mut txn = unsafe { self.userdb.get_rw_txn() }?;
self.userdb.clear_txn(&mut txn)?;
for (uid, data) in dump {
let user = db::User {
id: uid.clone(),
userdata: data.clone(),
};
tracing::trace!(%uid, ?user, "Storing user object");
if let Err(e) = self.userdb.put_txn(&mut txn, uid.as_str(), &user) {
tracing::warn!(error=?e, "failed to add user")
}
}
txn.commit().map_err(crate::db::Error::from)?;
Ok(())
}
pub fn dump_map(&self) -> miette::Result<HashMap<String, UserData>> {
return Ok(self.userdb.get_all()?)
}
pub fn dump_file(&self, path_str: &str, force: bool) -> miette::Result<usize> { pub fn dump_file(&self, path_str: &str, force: bool) -> miette::Result<usize> {
let path = Path::new(path_str); let path = Path::new(path_str);
let exists = path.exists(); let exists = path.exists();
@ -200,7 +226,7 @@ impl Users {
} }
let mut file = fs::File::create(path).into_diagnostic()?; let mut file = fs::File::create(path).into_diagnostic()?;
let users = self.userdb.get_all()?; let users = self.dump_map()?;
let encoded = toml::ser::to_vec(&users).into_diagnostic()?; let encoded = toml::ser::to_vec(&users).into_diagnostic()?;
file.write_all(&encoded[..]).into_diagnostic()?; file.write_all(&encoded[..]).into_diagnostic()?;

View File

@ -1,5 +1,5 @@
use clap::{Arg, Command, ValueHint}; use clap::{Arg, Command, ValueHint};
use diflouroborane::{config, Diflouroborane}; use difluoroborane::{config, Difluoroborane};
use std::str::FromStr; use std::str::FromStr;
use std::{env, io, io::Write, path::PathBuf}; use std::{env, io, io::Write, path::PathBuf};
@ -15,12 +15,12 @@ fn main() -> miette::Result<()> {
FabAccess {apiver}\n\ FabAccess {apiver}\n\
\t[{build_kind} build built on {build_time}]\n\ \t[{build_kind} build built on {build_time}]\n\
\t {rustc_version}\n\t {cargo_version}", \t {rustc_version}\n\t {cargo_version}",
version=diflouroborane::env::PKG_VERSION, version=difluoroborane::env::PKG_VERSION,
apiver="0.3", apiver="0.3",
rustc_version=diflouroborane::env::RUST_VERSION, rustc_version=difluoroborane::env::RUST_VERSION,
cargo_version=diflouroborane::env::CARGO_VERSION, cargo_version=difluoroborane::env::CARGO_VERSION,
build_time=diflouroborane::env::BUILD_TIME_3339, build_time=difluoroborane::env::BUILD_TIME_3339,
build_kind=diflouroborane::env::BUILD_RUST_CHANNEL)) build_kind=difluoroborane::env::BUILD_RUST_CHANNEL))
.about(clap::crate_description!()) .about(clap::crate_description!())
.arg(Arg::new("config") .arg(Arg::new("config")
.help("Path to the config file to use") .help("Path to the config file to use")
@ -57,10 +57,18 @@ fn main() -> miette::Result<()> {
.help("Check config for validity") .help("Check config for validity")
.long("check")) .long("check"))
.arg( .arg(
Arg::new("dump") Arg::new("dump-db")
.help("Dump all internal databases") .help("Dump all internal databases")
.long("dump") .long("dump-db")
.conflicts_with("load")) .alias("dump")
.conflicts_with("dump-users")
.conflicts_with("load-users")
.conflicts_with("load-db")
.takes_value(true)
.value_name("FILE")
.value_hint(ValueHint::AnyPath)
.default_missing_value("bffh-db.toml")
)
.arg( .arg(
Arg::new("dump-users") Arg::new("dump-users")
.help("Dump the users db to the given file as TOML") .help("Dump the users db to the given file as TOML")
@ -69,18 +77,33 @@ fn main() -> miette::Result<()> {
.value_name("FILE") .value_name("FILE")
.value_hint(ValueHint::AnyPath) .value_hint(ValueHint::AnyPath)
.default_missing_value("users.toml") .default_missing_value("users.toml")
.conflicts_with("load")) .conflicts_with("load-users")
.conflicts_with("load-db")
.conflicts_with("dump-db")
)
.arg( .arg(
Arg::new("force") Arg::new("force")
.help("force ops that may clobber") .help("force ops that may clobber")
.long("force") .long("force")
) )
.arg( .arg(
Arg::new("load") Arg::new("load-users")
.help("Load values into the internal databases") .help("Load users into the internal databases")
.long("load") .long("load-users")
.alias("load")
.takes_value(true) .takes_value(true)
.conflicts_with("dump")) .conflicts_with("dump-db")
.conflicts_with("load-db")
.conflicts_with("dump-users")
)
.arg(
Arg::new("load-db")
.help("Load values into the internal databases")
.long("load-db")
.takes_value(true)
.conflicts_with("dump-db")
.conflicts_with("load-users")
.conflicts_with("dump-users"))
.arg(Arg::new("keylog") .arg(Arg::new("keylog")
.help("log TLS keys into PATH. If no path is specified the value of the envvar SSLKEYLOGFILE is used.") .help("log TLS keys into PATH. If no path is specified the value of the envvar SSLKEYLOGFILE is used.")
.long("tls-key-log") .long("tls-key-log")
@ -98,7 +121,7 @@ fn main() -> miette::Result<()> {
let configpath = matches let configpath = matches
.value_of("config") .value_of("config")
.unwrap_or("/etc/diflouroborane.dhall"); .unwrap_or("/etc/difluoroborane.dhall");
// Check for the --print-default option first because we don't need to do anything else in that // Check for the --print-default option first because we don't need to do anything else in that
// case. // case.
@ -137,10 +160,18 @@ fn main() -> miette::Result<()> {
let mut config = config::read(&PathBuf::from_str(configpath).unwrap())?; let mut config = config::read(&PathBuf::from_str(configpath).unwrap())?;
if matches.is_present("dump") { if matches.is_present("dump-db") {
return Err(miette::miette!("DB Dumping is currently not implemented, except for the users db, using `--dump-users`")); let mut bffh = Difluoroborane::new(config)?;
let fname = matches.value_of("dump-db").unwrap();
bffh.dump_db(fname)?;
return Ok(());
} else if matches.is_present("load-db") {
let mut bffh = Difluoroborane::new(config)?;
let fname = matches.value_of("load-db").unwrap();
bffh.load_db(fname)?;
return Ok(());
} else if matches.is_present("dump-users") { } else if matches.is_present("dump-users") {
let bffh = Diflouroborane::new(config)?; let bffh = Difluoroborane::new(config)?;
let number = bffh.users.dump_file( let number = bffh.users.dump_file(
matches.value_of("dump-users").unwrap(), matches.value_of("dump-users").unwrap(),
@ -150,12 +181,12 @@ fn main() -> miette::Result<()> {
tracing::info!("successfully dumped {} users", number); tracing::info!("successfully dumped {} users", number);
return Ok(()); return Ok(());
} else if matches.is_present("load") { } else if matches.is_present("load-users") {
let bffh = Diflouroborane::new(config)?; let bffh = Difluoroborane::new(config)?;
bffh.users.load_file(matches.value_of("load").unwrap())?; bffh.users.load_file(matches.value_of("load-users").unwrap())?;
tracing::info!("loaded users from {}", matches.value_of("load").unwrap()); tracing::info!("loaded users from {}", matches.value_of("load-users").unwrap());
return Ok(()); return Ok(());
} else { } else {
@ -179,7 +210,7 @@ fn main() -> miette::Result<()> {
} }
config.logging.format = matches.value_of("log format").unwrap_or("full").to_string(); config.logging.format = matches.value_of("log format").unwrap_or("full").to_string();
let mut bffh = Diflouroborane::new(config)?; let mut bffh = Difluoroborane::new(config)?;
bffh.run()?; bffh.run()?;
} }

View File

@ -1,4 +1,4 @@
fn main() { fn main() {
// Extract build-time information using the `shadow-rs` crate // Extract build-time information using the `shadow-rs` crate
shadow_rs::new(); shadow_rs::new().unwrap();
} }

8
cargo-cross-config Normal file
View File

@ -0,0 +1,8 @@
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
[target.arm-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabi-gcc"
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

View File

@ -1,39 +0,0 @@
strict digraph connection {
Establish [label="TCP/SCTP connection established"];
Closed [label="TCP/SCTP connection closed"];
Open;
SASL;
Authenticated;
STARTTLS;
Encrypted;
Establish -> Open [label=open];
Open -> Closed [label=close];
Open -> SASL [label=auth];
SASL -> SASL [label=step];
// Authentication fails
SASL -> Closed [label=fails];
// Authentication succeeds
SASL -> Authenticated [label=successful];
Open -> STARTTLS [label=starttls];
// TLS wrapping succeeds
STARTTLS -> Encrypted [label=successful];
// TLS wrapping fails
STARTTLS -> Closed [label=fails];
Authenticated -> SASL_TLS [label=starttls];
SASL_TLS -> Closed [label=fails];
SASL_TLS -> AuthEnc [label=successful];
Encrypted -> TLS_SASL [label=auth];
TLS_SASL -> TLS_SASL [label=step];
TLS_SASL -> Closed [label=fails];
TLS_SASL -> AuthEnc [label=successful];
// Only authenticated connections may open RPC. For "unauth", use the `Anonymous` SASL method.
AuthEnc -> RPC [label=bootstrap];
Authenticated -> RPC [label=bootstrap];
}

View File

@ -1,42 +0,0 @@
# Stream initiation
In a session there are two parties: The initiating entity and the receiving
entity. This terminology does not refer to information flow but rather to the
side opening a connection respectively the one listening for connection
attempts.
In the currently envisioned use-case the initiating entity is a) a client
(i.e. interactive or batch/automated program) trying to interact in some way or
other with a server b) a server trying to exchange / request information
with/from another server (i.e. federating). The receiving entity however is
already a server.
Additionally the amount and type of clients is likely to be more diverse and
less up to date than the servers.
Conclusions I draw from this:
- Clients are more likely to implement an outdated version of the communication
protocol.
- The place for backwards-compatability should be the servers.
- Thus the client (initiating entity) should send the expected API version
first, the server then using that as a basis to decide with which API
version to answer.
# Stream negotiation
Since the receiving entity for a connection is responsible for the machines it
controls it imposes conditions for connecting either as client or as federating
server. At least every initiating entity is required to authenticate itself to
the receiving entity before attempting further actions or requesting
information. But a receiving entity can require other features, such as
transport layer encryption.
To this end a receiving entity informs the initiating entity about features that
it requires from the initiating entity before taking any further action and
features that are voluntary to negotiate but may improve qualities of the stream
(such as message compression)
A varying set of conditions implies negotiation needs to take place. Since
features potentially require a strict order (e.g. Encryption before
Authentication) negotiation has to be a multi-stage process. Further
restrictions are imposed because some features may only be offered after others
have been established (e.g. SASL authentication only becoming available after
encryption, EXTERNAL mechanism only being available to local sockets or
connections providing a certificate)

View File

@ -1,93 +0,0 @@
strict digraph state {
rank = 0
subgraph "cluster_internal_state" {
rank = 1
ctr = applied
start
[shape=doublecircle, label="BFFH"]
created
[label="Machine object created"];
start -> created;
created -> attach
[label="New state or loaded from disk"];
attach
[label="Attach actor", shape=box];
unapplied
[label="Unapplied"];
applied
[label="Applied"];
verified
[label="Verified"];
wait_apply
[label="Wait ∀ Actors", shape=box]
wait_verify
[label="Wait ∀ Actors", shape=box]
unapplied -> wait_apply -> applied;
applied -> wait_verify -> verified;
applied -> unapplied
[label="statechange received"];
verified -> unapplied
[label="statechange received"];
unapplied -> unapplied
[label="statechange received"];
unapplied -> attach -> unapplied;
applied -> attach -> unapplied;
verified -> attach -> unapplied;
}
subgraph "cluster_actor" {
rank = 1
center = actor_applied
actor_start
[shape=doublecircle, label="Actor"];
actor_fresh
[label="Actor was just constructed"];
actor_start -> actor_fresh;
actor_attached
[label="Attached"];
actor_unapplied
[label="Unapplied"];
actor_applied
[label="Applied"];
actor_verified
[label="Verified"];
wait_initial
[label="Recv", shape=box];
wait_state
[label="Recv", shape=box];
actor_fresh -> wait_initial -> actor_attached;
actor_attached -> actor_applied
[label="initialize/apply"];
actor_unapplied -> actor_applied
[label="apply"];
actor_applied -> actor_verified
[label="verify"];
actor_unapplied -> wait_state;
actor_applied -> wait_state;
actor_verified -> wait_state;
wait_state -> actor_unapplied;
}
attach -> wait_initial
[label="Send initial state to that actor", style=dotted]
unapplied -> wait_state
[label="Send new state to all actors", style=dotted];
actor_applied -> wait_apply
[label="Confirm apply", style=dotted];
actor_verified -> wait_verify
[label="Confirm verify", style=dotted];
}

View File

@ -1,11 +0,0 @@
# API-Testsetup
wirklich nur um das API zu testen. ATM implementiert: machines::* & machine::read, authenticate
1. Ein mosquitto o.ä MQTT Server starten
1. Datenbanken füllen: `cargo run -- -c examples/bffh.dhall --load=examples`
1. Daemon starten: `cargo run -- -c examples/bffh.dhall`
1. ???
1. PROFIT!
A dockerized version of this example can be found in the docker subdirectory

View File

@ -1,179 +0,0 @@
#!/usr/bin/env python3
import sys
import argparse
def on_free(args, actor_name):
"""
Function called when the state of the connected machine changes to Free
again
"""
if args.verbose > 2:
print("on_free called!")
if actor_name == "DoorControl1":
# Do whatever you want to do in case `DoorControl1` is returned back to free.
# Keep in mind that process actors should return quickly to not miss
# updates, so if you need to do things that take a while fork a new
# process e.g. with the `subprocess` Module
print("I'm locking door 1!")
pass
elif actor_name == "DoorControl2":
print("I'm locking door 2!")
pass # Close a different door
else:
if not args.quiet:
print("process called with unknown id %s for state `Free`" % actor_name)
# It's a good idea to exit with an error code in case something
# unexpected happens.
# The process module logs everything printed to stdout by actors into
# the server log, but marks them as `Error` in case the actor process
# exits with a code != 0, making debugging somewhat easier.
exit(-1)
def on_use(args, actor_name, user_id):
"""
Function called when an user takes control of the connected machine
user_id contains the UID of the user now using the machine
"""
if args.verbose > 2:
print("on_use called!")
if actor_name == "DoorControl1":
print("I'm opening door 1 for 10 seconds!")
pass # Open door one
elif actor_name == "DoorControl2":
print("I'm opening door 2 for 10 seconds!")
pass # Open a different door
else:
if not args.quiet:
print("process called with unknown id %s for state `InUse`" % actor_name)
# It's a good idea to exit with an error code in case something
# unexpected happens.
# The process module logs everything printed to stdout by actors into
# the server log, but marks them as `Error` in case the actor process
# exits with a code != 0, making debugging somewhat easier.
exit(-1)
def on_tocheck(args, actor_name, user_id):
"""
Function called when an user returns control and the connected machine is
configured to go to state `ToCheck` instead of `Free` in that case.
user_id contains the UID of the manager expected to check the machine.
The user that used the machine beforehand has to be taken from the last
user field using the API (via e.g. the mobile app)
"""
if args.verbose > 2:
print("on_tocheck called!")
if not args.quiet:
print("process called with unexpected combo id %s and state 'ToCheck'" % actor_name)
exit(-1)
def on_blocked(args, actor_name, user_id):
"""
Function called when an manager marks the connected machine as `Blocked`
user_id contains the UID of the manager that blocked the machine
"""
if args.verbose > 2:
print("on_blocked called!")
if not args.quiet:
print("process called with unexpected combo id %s and state 'Blocked'" % actor_name)
exit(-1)
def on_disabled(args, actor_name):
"""
Function called when the connected machine is marked `Disabled`
"""
if not args.quiet:
print("process called with unexpected combo id %s and state 'Disabled'" % actor_name)
exit(-1)
def on_reserve(args, actor_name, user_id):
"""
Function called when the connected machine has been reserved by somebody.
user_id contains the UID of the reserving user.
"""
if not args.quiet:
print("process called with unexpected combo id %s and state 'Reserved'" % actor_name)
exit(-1)
def main(args):
"""
Python example actor
This is an example how to use the `process` actor type to run a Python script.
"""
if args.verbose is not None:
if args.verbose == 1:
print("verbose output enabled")
elif args.verbose == 2:
print("loud output enabled!")
elif args.verbose == 3:
print("LOUD output enabled!!!")
elif args.verbose > 4:
print("Okay stop you're being ridiculous.")
sys.exit(-2)
else:
args.verbose = 0
# You could also check the actor name here and call different functions
# depending on that variable instead of passing it to the state change
# methods.
new_state = args.state
if new_state == "free":
on_free(args, args.name)
elif new_state == "inuse":
on_use(args, args.name, args.userid)
elif new_state == "tocheck":
on_tocheck(args, args.name, args.userid)
elif new_state == "blocked":
on_blocked(args, args.name, args.userid)
elif new_state == "disabled":
on_disabled(args, args.name)
elif new_state == "reserved":
on_reserve(args, args.name, args.userid)
else:
print("Process actor called with unknown state %s" % new_state)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
# Parameters are passed to the Process actor as follows:
# 1. the contents of params.args, split by whitespace as separate args
# 2. the configured id of the actor (e.g. "DoorControl1")
# 3. the new state as one of [free|inuse|tocheck|blocked|disabled|reserved]
parser.add_argument("-q", "--quiet", help="be less verbose", action="store_true")
parser.add_argument("-v", "--verbose", help="be more verbose", action="count")
parser.add_argument("name",
help="name of this actor as configured in bffh.dhall"
)
# We parse the new state using subparsers so that we only require a userid
# in case it's a state that sets one.
subparsers = parser.add_subparsers(required=True, dest="state")
parser_free = subparsers.add_parser("free")
parser_inuse = subparsers.add_parser("inuse")
parser_inuse.add_argument("userid", help="The user that is now using the machine")
parser_tocheck = subparsers.add_parser("tocheck")
parser_tocheck.add_argument("userid", help="The user that should go check the machine")
parser_blocked = subparsers.add_parser("blocked")
parser_blocked.add_argument("userid", help="The user that marked the machine as blocked")
parser_disabled = subparsers.add_parser("disabled")
parser_reserved = subparsers.add_parser("reserved")
parser_reserved.add_argument("userid", help="The user that reserved the machine")
args = parser.parse_args()
main(args)

View File

@ -1,4 +0,0 @@
#!/usr/bin/env bash
echo "Bash actor called with $@" > /tmp/act
echo "Bash actor called with: $@"

View File

@ -1,234 +0,0 @@
{- 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 = <STRING>, port = <INTEGER> }
-- 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 = "steak.fritz.box", 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",
-- Audit log path. Bffh will log state changes into this file, one per line.
-- Audit log entries are for now JSON:
-- {"timestamp":1641497361,"machine":"Testmachine","state":{"state":{"InUse":{"uid":"Testuser","subuid":null,"realm":null}}}}
auditlog_path = "/tmp/bffh.audit",
-- 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 = [<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"
]
}
},
-- 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"
}
},
-- 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_<id>" 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"
}
},
DoorControl1 = {
-- This actor calls the actor.py script in examples/
-- It gets passed it's own name, so you can have several actors
-- from the same script.
-- If you need to pass more arguments to the command you can use the `args` key in
-- `params` as is done with the actor `Bash`
module = "Process",
-- the `args` are passed in front of all other parameters so they are best suited to
-- optional parameters like e.g. the verboseness
params = { cmd = "./examples/actor.py", args = "-vvv" }
},
DoorControl2 = {
module = "Process",
params = { cmd = "./examples/actor.py" }
},
DoorControl3 = {
-- This is an example for how it looks like if an actor is misconfigured.
-- the actor.py doesn't know anything about DoorControl3 and, if this actor is enabled,
-- will return with an error showing up in the server logs.
module = "Process",
params = { cmd = "./examples/actor.py" }
},
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.
--initiators = { 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,5 +0,0 @@
# API-Testsetup, aber mit Docker
wirklich nur um das API zu testen. ATM implementiert: machines::* & machine::read, authenticate
* run `docker-compose up` in this directory

View File

@ -1,42 +0,0 @@
-- { actor_connections = [] : List { _1 : Text, _2 : Text }
{ actor_connections = [{ _1 = "Testmachine", _2 = "Actor" }]
, actors =
{ Actor = { module = "Shelly", params = {=} }
}
, init_connections = [] : List { _1 : Text, _2 : Text }
--, init_connections = [{ _1 = "Initiator", _2 = "Testmachine" }]
, initiators =
{ Initiator = { module = "Dummy", params = {=} }
}
, listens =
[ { address = "::", port = Some 59661 }
]
, machines =
{ Testmachine =
{ description = Some "A test machine"
, disclose = "lab.test.read"
, manage = "lab.test.admin"
, name = "Testmachine"
, read = "lab.test.read"
, write = "lab.test.write"
},
Another =
{ description = Some "Another test machine"
, disclose = "lab.test.read"
, manage = "lab.test.admin"
, name = "Another"
, read = "lab.test.read"
, write = "lab.test.write"
},
Yetmore =
{ description = Some "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://mqtt:1883"
, db_path = "/tmp/bffh"
}

View File

@ -1 +0,0 @@
Testuser = "secret"

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,11 +0,0 @@
[Testuser]
# Define them in roles.toml as well
roles = ["somerole/lmdb", "testrole/lmdb"]
# If two or more users want to use the same machine at once the higher prio
# wins
priority = 0
# You can add whatever random data you want.
# It will get stored in the `kv` field in UserData.
noot = "noot!"

View File

@ -1,13 +0,0 @@
version: "3.8"
services:
bffh:
image: registry.gitlab.com/fabinfra/fabaccess/bffh:dev-latest
ports:
- "59661:59661"
volumes:
# generate a sample config.toml by running "docker run registry.gitlab.com/fabinfra/fabaccess/bffh:dev-latest --print-default > examples/config.toml" from the project root. You may have to delete the ipv6 listen section.
- "./config:/etc/bffh"
links:
- mqtt
mqtt:
image: eclipse-mosquitto:1.6.13

View File

@ -1,11 +0,0 @@
# Integration tests with Docker
## How it works
* spawns 2 instances of our bffh container and required mqqt broker
* spawns an additional debian to run a shell
* the containers can reach each other by their hostname
## How to start
* run `docker-compose up --exit-code-from test-manager` in this directory
* this will kill all containers when

View File

@ -1,20 +0,0 @@
{ actor_connections = [{ _1 = "Testmachine", _2 = "Actor" }]
, actors =
{ Actor = { module = "Shelly", params = {=} }
}
, init_connections = [{ _1 = "Initiator", _2 = "Testmachine" }]
, initiators =
{ Initiator = { module = "Dummy", params = {=} }
}
, listens = [{ address = "::", port = Some 59661 }]
, machines =
{ Testmachine =
{ description = Some "A test machine"
, disclose = "lab.test.read"
, manage = "lab.test.admin"
, name = "Testmachine"
, read = "lab.test.read"
, write = "lab.test.write"
} }
, mqtt_url = "tcp://mqtt-a:1883"
}

View File

@ -1 +0,0 @@
Testuser = "secret"

View File

@ -1,20 +0,0 @@
[testrole]
name = "Testrole"
permissions = [
"lab.test.*"
]
[somerole]
name = "Somerole"
parents = ["testparent%lmdb"]
permissions = [
"lab.some.admin"
]
[testparent]
name = "Testparent"
permissions = [
"lab.some.write",
"lab.some.read",
"lab.some.disclose",
]

View File

@ -1,11 +0,0 @@
[Testuser]
# Define them in roles.toml as well
roles = []
# If two or more users want to use the same machine at once the higher prio
# wins
priority = 0
# You can add whatever random data you want.
# It will get stored in the `kv` field in UserData.
noot = "noot!"

View File

@ -1,20 +0,0 @@
{ actor_connections = [{ _1 = "Testmachine", _2 = "Actor" }]
, actors =
{ Actor = { module = "Shelly", params = {=} }
}
, init_connections = [{ _1 = "Initiator", _2 = "Testmachine" }]
, initiators =
{ Initiator = { module = "Dummy", params = {=} }
}
, listens = [{ address = "::", port = Some 59661 }]
, machines =
{ Testmachine =
{ description = Some "A test machine"
, disclose = "lab.test.read"
, manage = "lab.test.admin"
, name = "Testmachine"
, read = "lab.test.read"
, write = "lab.test.write"
} }
, mqtt_url = "tcp://mqtt-b:1883"
}

View File

@ -1 +0,0 @@
Testuser = "secret"

View File

@ -1,20 +0,0 @@
[testrole]
name = "Testrole"
permissions = [
"lab.test.*"
]
[somerole]
name = "Somerole"
parents = ["testparent%lmdb"]
permissions = [
"lab.some.admin"
]
[testparent]
name = "Testparent"
permissions = [
"lab.some.write",
"lab.some.read",
"lab.some.disclose",
]

View File

@ -1,11 +0,0 @@
[Testuser]
# Define them in roles.toml as well
roles = []
# If two or more users want to use the same machine at once the higher prio
# wins
priority = 0
# You can add whatever random data you want.
# It will get stored in the `kv` field in UserData.
noot = "noot!"

View File

@ -1,26 +0,0 @@
version: "3.8"
services:
bffh-a:
image: registry.gitlab.com/fabinfra/fabaccess/bffh:dev-latest
command: ["sh", "-c", "diflouroborane -c /etc/bffh/bffh.dhall --load=/etc/bffh; diflouroborane -c /etc/bffh/bffh.dhall"]
volumes:
# generate a sample config.toml by running "docker run registry.gitlab.com/fabinfra/fabaccess/bffh:dev-latest --print-default > examples/config.toml" from the project root. You may have to delete the ipv6 listen section.
- "./config_a:/etc/bffh"
links:
- mqtt-a
mqtt-a:
image: eclipse-mosquitto
bffh-b:
image: registry.gitlab.com/fabinfra/fabaccess/bffh:dev-latest
command: ["sh", "-c", "diflouroborane -c /etc/bffh/bffh.dhall --load=/etc/bffh; diflouroborane -c /etc/bffh/bffh.dhall"]
volumes:
# generate a sample config.toml by running "docker run registry.gitlab.com/fabinfra/fabaccess/bffh:dev-latest --print-default > examples/config.toml" from the project root. You may have to delete the ipv6 listen section.
- "./config_b:/etc/bffh"
links:
- mqtt-b
mqtt-b:
image: eclipse-mosquitto
test-manager:
image: debian
tty: true

View File

@ -1,4 +0,0 @@
#!/usr/bin/env bash
echo "This is some error output" > /dev/stderr
exit 115

View File

@ -1,13 +0,0 @@
#!/usr/bin/env python
import sys
import time
while True:
print('{ "state": { "1.3.6.1.4.1.48398.612.2.4": { "state": "Free" } } }')
sys.stdout.flush()
time.sleep(2)
print('{ "state": { "1.3.6.1.4.1.48398.612.2.4": { "state": { "InUse": { "id": "Testuser" } } } } }')
sys.stdout.flush()
time.sleep(2)

View File

@ -1,19 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDFzCCAf+gAwIBAgIUDr+1F3zzyza+soLtiurKEXW9pGIwDQYJKoZIhvcNAQEL
BQAwGzEZMBcGA1UEAwwQYmZmaC1kZXZlbG9wbWVudDAeFw0yMTEyMDkxODQ2Mjla
Fw0zMTEyMDkxODQ2MjlaMBsxGTAXBgNVBAMMEGJmZmgtZGV2ZWxvcG1lbnQwggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGwNy7yGaURR08dWfoDmnJeyx1
0FVRmozoGCIb3Oj6c2t+84QUxqTdknE7Cdcz5Wi1o0x2CWPZG4z1vgTaCcJVhcME
hxn+7eK1NtDQEjs8Ojs7uaraVvooIe8R7jat0qs7Dmf8RO9P0I4MMZlvijhI7aLw
0C6vNsr1ebeppIiwO5aUuCGuKqxJGghHeqZv18ZcPayunyNrxMC6uyX7y6nUVkfq
x0m9gDsN112Iv9Dd/ZE5Gxivm8jZvVUGZgJD2szK7zbeCDeo5aU3cRWfYaoN0QDx
AKmo4bjciJzfMDDgvcIBq9lGS3FxEv394Mc5YX/ZdP+KRTiHcYCXfBzr3B6HAgMB
AAGjUzBRMB0GA1UdDgQWBBTtUvvWXlo5tU8cEoxbs5UJdodOVDAfBgNVHSMEGDAW
gBTtUvvWXlo5tU8cEoxbs5UJdodOVDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
DQEBCwUAA4IBAQAB3IxRnWi/LrxCvlHNaWEi3ZvlbN+KpegWZeKtjHwQoizhR/Fi
SMC7z4y6trJE7LXUOSb9Pu8QQSvpVQZd3W4XCPZMl10Lt7iV8vc3pVviCseDnT9r
X1gXdbeuyYm9lE8KtlhX03jD/CiEx7Qe/q8Rc20AQIKAAJzvl7sXU2tmJ5kpzMEO
v5czlLaX2ajlD/QMgNBUuDyw6wPo3wx9Khph484RygN2LHeT6kfu/PBiF0dGDTUu
mgxg4K0GfwTcHgtz5Bag/HyuuJEKx8Wv7jth59PyKPT+lMVBznxIm3gLS5U+Nnr1
uAws8dgLXRlPo5HJORuJCcZWVBClruZUpyDT
-----END CERTIFICATE-----

View File

@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDGwNy7yGaURR08
dWfoDmnJeyx10FVRmozoGCIb3Oj6c2t+84QUxqTdknE7Cdcz5Wi1o0x2CWPZG4z1
vgTaCcJVhcMEhxn+7eK1NtDQEjs8Ojs7uaraVvooIe8R7jat0qs7Dmf8RO9P0I4M
MZlvijhI7aLw0C6vNsr1ebeppIiwO5aUuCGuKqxJGghHeqZv18ZcPayunyNrxMC6
uyX7y6nUVkfqx0m9gDsN112Iv9Dd/ZE5Gxivm8jZvVUGZgJD2szK7zbeCDeo5aU3
cRWfYaoN0QDxAKmo4bjciJzfMDDgvcIBq9lGS3FxEv394Mc5YX/ZdP+KRTiHcYCX
fBzr3B6HAgMBAAECggEAS5DGG6ssvRCt9e+ZatQYCl+HXt+voJAHJLMQPNG3zokV
hLXnMNL5mbh0zoKGTJfbQLvudS5KxR/BbykoxRFSzptFszH+gztEp6tIpuNXnCVz
odiMiejpwVptf763EU14hsKKbJJ0/j6H00EEWjEOB0Q6YB52sW0+qyf02U3SHlZN
k2PZYkpHi3YCONtOGj7jOdW7M3RfvcBNg9EW7fZc1KkiRAlscUAYLMkKcOLevil0
lUuF/JWj4iH22Oq6+JeSiecf6izF6lyIGvMXPry+woa8Iq0BBdmbZsK7r/Pa+wlz
+E6xHGn2rcyrnYB2pPc+RfHhYlodaOo69DxAYlRYaQKBgQDxnKySmlcfHe8lMqR4
2LvqMyGjNRo2+q9VZYej2mvr6+TGyd7Op/fRJ1t5f9DgtINomNQYSOtYAsPiEnl+
43z2N/Rdh6XSmOj4gLkDeiYNSpy86L/F39uCWZpkkqiy2zxYLWOs15MA0GWOtQGh
dz4cM0b/jyZOdHZR//L5WiMLawKBgQDSltEfQKqCEKJTqp4SU9BCHQmyheIjepY2
eKakgcsjpFjRBK2VrUalDLOQV74rtd7wp8F8kqqPJpRshb+oVuDCg6JB17UxYd34
iB+5cMdLRpg8f0HOqcYz4KOql2QhJQhFc3jzY7n1piPEhFO/MqFwmlUB4RdgJ3ix
HqX+F/T8VQKBgGos76l9KcwC25T9LEnu9KV20tFmBJ8kiuh8NZ9L3SFQCLlS/RbT
uZOwOAKsqJ4WtajBgHMrmECU9n/inoGkdsW80SZI9hYWHEsYRjXA9/ffUgGyRpQu
S8h8l9yalogC0AHv8F2EXpV8/yQ3ZwAN5r19yzWDMtJHW7etQplRgxUBAoGAGeOy
t+3iSHU1D6YlIsmtC8O4Int1LrluaCnzCrxuNeaJiMDTelhAHCBwnuk6lvMYAmwN
THxXfZvXmXPj+RUdMqyuMPwM6ZJHkLtjcw/bYHTAWIeolnimxk/yrxFHnQ+Jcchd
cUasYPfY49sE1Lerw0Ul+EIs9oRDwTqsW42kb7UCgYEA2y+oc7Fz2eq38hSbTfy7
OcATtny+xQ1+4IELtQIP7VctkMInJs57J+vS//IT41P0L2K1YjvL8RacnvG7yMvP
GnwHBcKgvL6zuoy11I3zPPYtbKGwcJoVGomPX7W0csfl4gdST3uugd9iCDEB8NsS
QmOYM/dk8x8aWpndBjRF5ig=
-----END PRIVATE KEY-----

View File

@ -1,16 +0,0 @@
[Testuser]
# 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", "testrole"]
# 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!"
# Store the card specific AES key in kv userdata
cardkey = "7ab8704a61b5317e1fe4cae9e3e1fd8d"

View File

@ -8,4 +8,4 @@ edition = "2021"
[dependencies] [dependencies]
sdk-proc = { path = "sdk_proc" } sdk-proc = { path = "sdk_proc" }
futures-util = "0.3" futures-util = "0.3"
diflouroborane = { path = "../.." } difluoroborane = { path = "../.." }

View File

@ -30,7 +30,8 @@ where
} }
let schedule = |t| (QUEUE.deref()).send(t).unwrap(); let schedule = |t| (QUEUE.deref()).send(t).unwrap();
let (proc, handle) = LightProc::recoverable(future, schedule); let span = tracing::trace_span!("runtime.spawn", kind = "local");
let (proc, handle) = LightProc::recoverable(future, schedule, span, None);
let handle = handle.on_panic( let handle = handle.on_panic(
|err: Box<dyn Any + Send>| match err.downcast::<&'static str>() { |err: Box<dyn Any + Send>| match err.downcast::<&'static str>() {

View File

@ -17,7 +17,8 @@ where
let future = async move { fut.await }; let future = async move { fut.await };
let schedule = move |t| sender.send(t).unwrap(); let schedule = move |t| sender.send(t).unwrap();
let (proc, handle) = LightProc::build(future, schedule); let span = tracing::trace_span!("runtime.spawn", kind = "local");
let (proc, handle) = LightProc::build(future, schedule, span, None);
proc.schedule(); proc.schedule();

View File

@ -9,6 +9,7 @@
//! # Example Usage //! # Example Usage
//! //!
//! ```rust //! ```rust
//! use tracing::Span;
//! use lightproc::prelude::*; //! use lightproc::prelude::*;
//! //!
//! // ... future that does work //! // ... future that does work
@ -23,6 +24,8 @@
//! let panic_recoverable = LightProc::recoverable( //! let panic_recoverable = LightProc::recoverable(
//! future, //! future,
//! schedule_function, //! schedule_function,
//! Span::current(),
//! None,
//! ); //! );
//! ``` //! ```
@ -60,6 +63,7 @@ impl LightProc {
/// # Example /// # Example
/// ```rust /// ```rust
/// # use std::any::Any; /// # use std::any::Any;
/// # use tracing::Span;
/// # use lightproc::prelude::*; /// # use lightproc::prelude::*;
/// # /// #
/// # // ... basic schedule function with no waker logic /// # // ... basic schedule function with no waker logic
@ -72,9 +76,11 @@ impl LightProc {
/// let (proc, handle) = LightProc::recoverable( /// let (proc, handle) = LightProc::recoverable(
/// future, /// future,
/// schedule_function, /// schedule_function,
/// Span::current(),
/// None
/// ); /// );
/// let handle = handle.on_panic(|s: &mut EmptyProcState, e: Box<dyn Any + Send>| { /// let handle = handle.on_panic(|e: Box<dyn Any + Send>| {
/// let reason = e.downcast::<String>(); /// let reason = e.downcast::<String>().unwrap();
/// println!("future panicked!: {}", &reason); /// println!("future panicked!: {}", &reason);
/// }); /// });
/// ``` /// ```
@ -110,13 +116,6 @@ impl LightProc {
/// # // ... basic schedule function with no waker logic /// # // ... basic schedule function with no waker logic
/// # fn schedule_function(proc: LightProc) {;} /// # fn schedule_function(proc: LightProc) {;}
/// # /// #
/// # // ... process stack with a lifecycle callback
/// # let proc_stack =
/// # ProcStack::default()
/// # .with_after_panic(|s: &mut EmptyProcState| {
/// # println!("After panic started!");
/// # });
/// #
/// // ... creating a standard process /// // ... creating a standard process
/// let standard = LightProc::build( /// let standard = LightProc::build(
/// future, /// future,

View File

@ -395,7 +395,7 @@ where
unsafe fn tick(ptr: *const ()) { unsafe fn tick(ptr: *const ()) {
let mut raw = Self::from_ptr(ptr); let mut raw = Self::from_ptr(ptr);
// Enter the span associated with the process to track execution time if enabled. // Enter the span associated with the process to track execution time if enabled.
let _guard = (&(*raw.pdata).span).enter(); let guard = (&(*raw.pdata).span).enter();
// Create a context from the raw proc pointer and the vtable inside its pdata. // Create a context from the raw proc pointer and the vtable inside its pdata.
let waker = ManuallyDrop::new(Waker::from_raw(RawWaker::new( let waker = ManuallyDrop::new(Waker::from_raw(RawWaker::new(
@ -487,6 +487,8 @@ where
(*raw.pdata).notify(); (*raw.pdata).notify();
} }
// the tracing guard is inside the proc, so it must be dropped first
drop(guard);
// Drop the proc reference. // Drop the proc reference.
Self::decrement(ptr); Self::decrement(ptr);
break; break;

View File

@ -49,8 +49,7 @@ impl<R> RecoverableHandle<R> {
/// ///
/// ```rust /// ```rust
/// # use std::any::Any; /// # use std::any::Any;
/// use lightproc::proc_stack::ProcStack; /// # use tracing::Span;
/// use lightproc::proc_state::EmptyProcState;
/// # use lightproc::prelude::*; /// # use lightproc::prelude::*;
/// # /// #
/// # // ... future that does work /// # // ... future that does work
@ -61,21 +60,16 @@ impl<R> RecoverableHandle<R> {
/// # // ... basic schedule function with no waker logic /// # // ... basic schedule function with no waker logic
/// # fn schedule_function(proc: LightProc) {;} /// # fn schedule_function(proc: LightProc) {;}
/// # /// #
/// # // ... process stack with a lifecycle callback
/// # let proc_stack =
/// # ProcStack::default()
/// # .with_after_panic(|s: &mut EmptyProcState| {
/// # println!("After panic started!");
/// # });
/// #
/// // ... creating a recoverable process /// // ... creating a recoverable process
/// let (proc, recoverable) = LightProc::recoverable( /// let (proc, recoverable) = LightProc::recoverable(
/// future, /// future,
/// schedule_function, /// schedule_function,
/// Span::current(),
/// None
/// ); /// );
/// ///
/// recoverable /// recoverable
/// .on_return(|_e: Box<dyn Any + Send>| { /// .on_panic(|_e: Box<dyn Any + Send>| {
/// println!("Inner future panicked"); /// println!("Inner future panicked");
/// }); /// });
/// ``` /// ```

2
rust-toolchain.toml Normal file
View File

@ -0,0 +1,2 @@
[toolchain]
channel = "1.66"

View File

@ -1,8 +0,0 @@
[UniqueUser]
roles = ["foorole", "barrole"]
[DuplicateUser]
roles = ["somerole"]
[DuplicateUser]
roles = ["different", "roles"]