Compare commits

..

3 commits

Author SHA1 Message Date
ToxicMushroom 26cbb2d8c1
Add primary email selection, update email add button. Use scim. 2025-04-05 03:02:22 +02:00
ToxicMushroom e2c6190693
patch up rebase 2025-03-06 02:28:26 +01:00
ToxicMushroom a28c4e8cd4
- Form validation
- Editable emails
- Basic profile updating
2025-03-06 02:28:23 +01:00
109 changed files with 4170 additions and 4512 deletions
.github/workflows
Cargo.lockCargo.toml
book/src
examples
libs
client/src
profiles
platform
proto/src
pykanidm
scripts
server
shell.nix
tools
cli/src
cli/group
opt
iam_migrations
unix_integration

View file

@ -19,7 +19,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9
uses: mozilla-actions/sccache-action@v0.0.7
with:
version: "v0.4.2"
- name: Install dependencies
run: |
sudo apt-get update && \
@ -39,6 +41,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9
uses: mozilla-actions/sccache-action@v0.0.7
with:
version: "v0.4.2"
- name: "Run cargo fmt"
run: cargo fmt --check

View file

@ -24,7 +24,9 @@ jobs:
with:
ref: ${{ inputs.tag }}
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9
uses: mozilla-actions/sccache-action@v0.0.7
with:
version: "v0.4.2"
- name: Install deps
run: |
sudo apt-get update

View file

@ -27,7 +27,10 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9
uses: mozilla-actions/sccache-action@v0.0.7
with:
version: "v0.4.2"
- name: Install dependencies
run: |
sudo apt-get update && \
@ -72,7 +75,10 @@ jobs:
with:
toolchain: ${{ matrix.rust_version }}
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9
uses: mozilla-actions/sccache-action@v0.0.7
with:
version: "v0.4.2"
- name: Install dependencies
run: |
sudo apt-get update && \
@ -112,7 +118,10 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9
uses: mozilla-actions/sccache-action@v0.0.7
with:
version: "v0.4.2"
- name: Install dependencies
run: |
sudo apt-get update && \

View file

@ -28,7 +28,9 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9
uses: mozilla-actions/sccache-action@v0.0.7
with:
version: "v0.4.2"
- run: cargo build --locked -p kanidm_client -p kanidm_tools --bin kanidm
# yamllint disable-line rule:line-length
- run: cargo test -p kanidm_client -p kanidm_tools

903
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -159,12 +159,12 @@ base64 = "^0.22.1"
base64urlsafedata = "0.5.1"
bitflags = "^2.8.0"
bytes = "^1.9.0"
clap = { version = "^4.5.34", features = ["derive", "env"] }
clap = { version = "^4.5.27", features = ["derive", "env"] }
clap_complete = "^4.5.42"
# Forced by saffron/cron
chrono = "^0.4.39"
compact_jwt = { version = "^0.4.2", default-features = false }
concread = "^0.5.5"
concread = "^0.5.3"
cron = "0.15.0"
crossbeam = "0.8.4"
csv = "1.3.1"
@ -190,7 +190,7 @@ image = { version = "0.24.9", default-features = false, features = [
"jpeg",
"webp",
] }
itertools = "0.14.0"
itertools = "0.13.0"
enum-iterator = "2.1.0"
kanidmd_web_ui_shared = { path = "./server/web_ui/shared" }
# REMOVE this
@ -202,11 +202,11 @@ libc = "^0.2.168"
libnss = "^0.8.0"
libsqlite3-sys = "^0.25.2"
lodepng = "3.11.0"
lru = "^0.13.0"
lru = "^0.12.5"
mathru = "^0.13.0"
md-5 = "0.10.6"
mimalloc = "0.1.43"
notify-debouncer-full = { version = "0.5" }
notify-debouncer-full = { version = "0.1" }
num_enum = "^0.5.11"
oauth2_ext = { version = "^4.4.2", package = "oauth2", default-features = false }
openssl-sys = "^0.9"
@ -294,7 +294,7 @@ webauthn-rs = { version = "0.5.1", features = ["preview-features"] }
webauthn-rs-core = "0.5.1"
webauthn-rs-proto = "0.5.1"
whoami = "^1.6.0"
whoami = "^1.5.2"
walkdir = "2"
x509-cert = "0.2.5"

View file

@ -53,6 +53,7 @@
- [Service Integration Examples](examples/readme.md)
- [Kubernetes Ingress](examples/kubernetes_ingress.md)
- [OAuth2 Examples](integrations/oauth2/examples.md)
- [Traefik](examples/traefik.md)
- [Replication](repl/readme.md)

View file

@ -58,21 +58,6 @@ can only use the UID range `1879048192` (`0x70000000`) to `2147483647` (`0x7ffff
limitations of the Linux kernel and
[systemd reserving other uids in the range](http://systemd.io/UIDS-GIDS/) for its exclusive use.
| name | min | max |
|------|-----|-----|
| system | 0 | 999 |
| user | 1000 | 60000 |
| systemd homed | 60001 | 60577 |
| unused A | 60578 | 61183 |
| systemd dynuser | 61184 | 65519 |
| unused B | 65520 | 65533 |
| nobody | 65534 | 65534 |
| 16bit limit | 65535 | 65535 |
| unused C | 65536 | 524287 |
| systemd nspawn | 524288 | 1879048191 |
| kanidm dyn alloc | 1879048192 | 2147483647 |
| unusable | 2147483648 | 4294967295 |
A valid concern is the possibility of duplication in the lower 24 bits. Given the
[birthday problem](https://en.wikipedia.org/wiki/Birthday_problem), if you have ~7700 groups and
accounts, you have a 50% chance of duplication. With ~5000 you have a 25% chance, ~930 you have a 1%
@ -82,7 +67,7 @@ We advise that if you have a site with greater than approximately 2,000 users yo
external system to allocate GID numbers serially or consistently to avoid potential duplication
events.
We recommend the use of the range `65536` through `524287` (`unused C`) for manual allocation. This leaves the
We recommend the use of the range `65536` through `524287` for manual allocation. This leaves the
range `1000` through `65535` for OS/Distro purposes, and allows Kanidm to continue dynamic
allocation in the range `1879048192` to `2147483647` if you choose a hybrid allocation approach.

View file

@ -45,7 +45,6 @@ can take many forms such as.
- firstname firstname lastname
- firstname lastname lastname
- firstname
- middlename lastname
- lastname firstname
And many many more that are not listed here. This is why our names are displayName as a freetext

View file

@ -54,6 +54,7 @@ services:
- traefik.http.routers.kanidm.entrypoints=websecure
- traefik.http.routers.kanidm.rule=Host(`idm.example.com`)
- traefik.http.routers.kanidm.service=kanidm
- traefik.http.serversTransports.kanidm.insecureSkipVerify=true
- traefik.http.services.kanidm.loadbalancer.server.port=8443
- traefik.http.services.kanidm.loadbalancer.server.scheme=https
volumes:

View file

@ -566,7 +566,7 @@ Due to a [lack of public client support](https://github.com/oauth2-proxy/oauth2-
```bash
kanidm system oauth2 create webapp 'webapp.example.com' 'https://webapp.example.com'
kanidm system oauth2 add-redirect-url webapp 'https://webapp.example.com/oauth2/callback'
kanidm system add-redirect-url webapp 'https://webapp.example.com/oauth2/callback'
kanidm system oauth2 update-scope-map webapp email openid
kanidm system oauth2 get webapp
kanidm system oauth2 show-basic-secret webapp

View file

@ -5,45 +5,57 @@
- Debian packaging is complex enough that it lives in a separate repository:
[kanidm/kanidm_ppa_automation](https://github.com/kanidm/kanidm_ppa_automation).
- While official packages are available at https://kanidm.github.io/kanidm_ppa/ these instructions will guide you
through replicating the same process locally, using Docker to isolate the build process from your normal computer.
- Due to the complexity of crosscompilation, we no longer support it and recommend building natively,
i.e. on the platform you're targeting.
through replicating the same process locally, using [cross](https://github.com/cross-rs/cross) & Docker to isolate the build process
from your normal computer and allow building packages for multiple architectures.
- While the examples below will use `aarch64-unknown-linux-gnu` aka `arm64`,
the same process works for `x86_64-unknown-linux-gnu` aka `amd64` as well.
1. Start in the root directory of the main [kanidm/kanidm](https://github.com/kanidm/kanidm) repository.
1. Install cross:
```shell
cargo install cross
```
1. Pull in the separate deb packaging submodule:
```shell
git submodule update platform/debian/kanidm_ppa_automation
```
1. Launch your desired crossbuild target. Do note the script assumes you use rustup!
```shell
# See valid targets:
platform/debian/kanidm_ppa_automation/scripts/crossbuild.sh
# Launch a target:
platform/debian/kanidm_ppa_automation/scripts/crossbuild.sh debian-12-aarch64-unknown-linux-gnu
# You can also specify multiple targets within the same distribution:
platform/debian/kanidm_ppa_automation/scripts/crossbuild.sh debian-12-{aarch64,x86_64}-unknown-linux-gnu
```
1. Go get a drink of your choice while the build completes.
1. Create a sacrificial deb builder container to avoid changing your own system:
```shell
docker run --rm -it -e VERBOSE=true -e CI=true \
docker run --rm -it -e CI=true \
--mount "type=bind,src=$PWD,target=/src" \
--workdir /src \
rust:bookworm
```
1. In the container install dependencies with:
```shell
platform/debian/kanidm_ppa_automation/scripts/install_ci_build_dependencies.sh
# The parameter given is which additional target debian architecture to enable (amd64, arm64, etc.)
# If your native platform is amd64, running with arm64 is enough to cover both archs.
platform/debian/kanidm_ppa_automation/scripts/install_ci_build_dependencies.sh arm64
```
1. Launch your desired target build:
```shell
platform/debian/kanidm_ppa_automation/scripts/build_native.sh aarch64-unknown-linux-gnu
```
1. Go get a drink of your choice while the build completes.
1. Launch the deb build:
1. In the container launch the deb build:
```shell
platform/debian/kanidm_ppa_automation/scripts/build_debs.sh aarch64-unknown-linux-gnu
# Again, multiple targets also work:
platform/debian/kanidm_ppa_automation/scripts/build_debs.sh {aarch64,x86_64}-unknown-linux-gnu
```
1. You can now exit the container, the package paths displayed at the end under `target` will
persist.
## Adding or amending a deb package
The rough overview of steps is as follows, see further down for details.
The rough overview of steps is:
1. Add cargo-deb specific metadata to the rust package and any static assets. Submit your changes as
a PR.
2. Add build steps to the separate packaging repo. Submit your changes as a PR.
2. Add build instructions to the separate packaging repo. Submit your changes as a PR.
3. Go back to the main repo to update the packaging submodule reference to aid running manual dev
builds of the new package.
@ -60,8 +72,8 @@ an example, see `unix_integration/resolver/Cargo.toml`
### Configuration in the kanidm_ppa_automation repo
- The repo is: [kanidm/kanidm_ppa_automation](https://github.com/kanidm/kanidm_ppa_automation)
- Changes are needed if a new binary and/or package is added, or if build time dependencies change.
- Amend `scripts/build_native.sh` build rules to include new binaries or packages with shared
libraries.
- Amend `scripts/crossbuild.sh` build rules to include new binaries or packages with shared
libraries. Search for the lines starting with `cross build`.
- Add any new build time system dependencies to `scripts/install_ci_build_dependencies.sh`, be aware
of any difference in package names between Debian & Ubuntu.
- Add any new packages to `scripts/build_debs.sh`, search for the line starting with `for package in`.

View file

@ -1,4 +1,3 @@
version = "2"
bindaddress = "[::]:8443"
ldapbindaddress = "127.0.0.1:3636"

View file

@ -1,6 +1,3 @@
# The server configuration file version.
version = "2"
# The webserver bind address. Requires TLS certificates.
# If the port is set to 443 you may require the
# NET_BIND_SERVICE capability.

View file

@ -1,6 +1,3 @@
# The server configuration file version.
version = "2"
# The webserver bind address. Requires TLS certificates.
# If the port is set to 443 you may require the
# NET_BIND_SERVICE capability.

View file

@ -195,20 +195,4 @@ impl KanidmClient {
self.perform_get_request(&format!("/v1/group/{}/_attr/mail", id))
.await
}
pub async fn idm_group_purge_description(&self, id: &str) -> Result<(), ClientError> {
self.idm_group_purge_attr(id, "description").await
}
pub async fn idm_group_set_description(
&self,
id: &str,
description: &str,
) -> Result<(), ClientError> {
self.perform_put_request(
&format!("/v1/group/{}/_attr/description", id),
&[description],
)
.await
}
}

View file

@ -1,14 +0,0 @@
# The main difference from the release_linux profile is using
# per-package shared directories for a clearer separation and
# thus more consistent install & sysadmin experience.
# Don't set the value for autodetect
# cpu_flags = "none"
server_admin_bind_path = "/var/run/kanidmd/sock"
server_ui_pkg_path = "/usr/share/kanidmd/static"
server_config_path = "/etc/kanidmd/server.toml"
client_config_path = "/etc/kanidm/config"
# TODO: unixd should migrate to it's own config dir as part of the sparkled migration.
# No point in doing two back to back migrations.
resolver_config_path = "/etc/kanidm/unixd"
resolver_unix_shell_path = "/bin/bash"

@ -1 +1 @@
Subproject commit 8d7579fb543632df74e609892c69ce9f368fdd02
Subproject commit 942c7b69ca807cc38186b63ab02a391bac9eac7e

View file

@ -12,7 +12,7 @@ Conflicts=nscd.service
[Service]
DynamicUser=yes
SupplementaryGroups=tss
SupplementaryGroups=tss shadow
UMask=0027
CacheDirectory=kanidm-unixd
RuntimeDirectory=kanidm-unixd

View file

@ -22,8 +22,6 @@ pub enum Attribute {
AcpCreateClass,
AcpEnable,
AcpModifyClass,
AcpModifyPresentClass,
AcpModifyRemoveClass,
AcpModifyPresentAttr,
AcpModifyRemovedAttr,
AcpReceiver,
@ -82,7 +80,6 @@ pub enum Attribute {
IdVerificationEcKey,
Image,
Index,
Indexed,
IpaNtHash,
IpaSshPubKey,
JwsEs256PrivateKey,
@ -257,8 +254,6 @@ impl Attribute {
Attribute::AcpCreateClass => ATTR_ACP_CREATE_CLASS,
Attribute::AcpEnable => ATTR_ACP_ENABLE,
Attribute::AcpModifyClass => ATTR_ACP_MODIFY_CLASS,
Attribute::AcpModifyPresentClass => ATTR_ACP_MODIFY_PRESENT_CLASS,
Attribute::AcpModifyRemoveClass => ATTR_ACP_MODIFY_REMOVE_CLASS,
Attribute::AcpModifyPresentAttr => ATTR_ACP_MODIFY_PRESENTATTR,
Attribute::AcpModifyRemovedAttr => ATTR_ACP_MODIFY_REMOVEDATTR,
Attribute::AcpReceiver => ATTR_ACP_RECEIVER,
@ -316,7 +311,6 @@ impl Attribute {
Attribute::IdVerificationEcKey => ATTR_ID_VERIFICATION_ECKEY,
Attribute::Image => ATTR_IMAGE,
Attribute::Index => ATTR_INDEX,
Attribute::Indexed => ATTR_INDEXED,
Attribute::IpaNtHash => ATTR_IPANTHASH,
Attribute::IpaSshPubKey => ATTR_IPASSHPUBKEY,
Attribute::JwsEs256PrivateKey => ATTR_JWS_ES256_PRIVATE_KEY,
@ -444,8 +438,6 @@ impl Attribute {
ATTR_ACP_CREATE_CLASS => Attribute::AcpCreateClass,
ATTR_ACP_ENABLE => Attribute::AcpEnable,
ATTR_ACP_MODIFY_CLASS => Attribute::AcpModifyClass,
ATTR_ACP_MODIFY_PRESENT_CLASS => Attribute::AcpModifyPresentClass,
ATTR_ACP_MODIFY_REMOVE_CLASS => Attribute::AcpModifyRemoveClass,
ATTR_ACP_MODIFY_PRESENTATTR => Attribute::AcpModifyPresentAttr,
ATTR_ACP_MODIFY_REMOVEDATTR => Attribute::AcpModifyRemovedAttr,
ATTR_ACP_RECEIVER => Attribute::AcpReceiver,
@ -503,7 +495,6 @@ impl Attribute {
ATTR_ID_VERIFICATION_ECKEY => Attribute::IdVerificationEcKey,
ATTR_IMAGE => Attribute::Image,
ATTR_INDEX => Attribute::Index,
ATTR_INDEXED => Attribute::Indexed,
ATTR_IPANTHASH => Attribute::IpaNtHash,
ATTR_IPASSHPUBKEY => Attribute::IpaSshPubKey,
ATTR_JWS_ES256_PRIVATE_KEY => Attribute::JwsEs256PrivateKey,

View file

@ -62,8 +62,6 @@ pub const ATTR_ACP_CREATE_ATTR: &str = "acp_create_attr";
pub const ATTR_ACP_CREATE_CLASS: &str = "acp_create_class";
pub const ATTR_ACP_ENABLE: &str = "acp_enable";
pub const ATTR_ACP_MODIFY_CLASS: &str = "acp_modify_class";
pub const ATTR_ACP_MODIFY_PRESENT_CLASS: &str = "acp_modify_present_class";
pub const ATTR_ACP_MODIFY_REMOVE_CLASS: &str = "acp_modify_remove_class";
pub const ATTR_ACP_MODIFY_PRESENTATTR: &str = "acp_modify_presentattr";
pub const ATTR_ACP_MODIFY_REMOVEDATTR: &str = "acp_modify_removedattr";
pub const ATTR_ACP_RECEIVER_GROUP: &str = "acp_receiver_group";
@ -126,7 +124,6 @@ pub const ATTR_GROUP: &str = "group";
pub const ATTR_ID_VERIFICATION_ECKEY: &str = "id_verification_eckey";
pub const ATTR_IMAGE: &str = "image";
pub const ATTR_INDEX: &str = "index";
pub const ATTR_INDEXED: &str = "indexed";
pub const ATTR_IPANTHASH: &str = "ipanthash";
pub const ATTR_IPASSHPUBKEY: &str = "ipasshpubkey";
pub const ATTR_JWS_ES256_PRIVATE_KEY: &str = "jws_es256_private_key";

View file

@ -33,7 +33,7 @@ pub enum ScimAttributeEffectiveAccess {
/// All attributes on the entry have this permission granted
Grant,
/// All attributes on the entry have this permission denied
Deny,
Denied,
/// The following attributes on the entry have this permission granted
Allow(BTreeSet<Attribute>),
}
@ -43,7 +43,7 @@ impl ScimAttributeEffectiveAccess {
pub fn check(&self, attr: &Attribute) -> bool {
match self {
Self::Grant => true,
Self::Deny => false,
Self::Denied => false,
Self::Allow(set) => set.contains(attr),
}
}

724
pykanidm/poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -29,7 +29,7 @@ Authlib = "^1.2.0"
[tool.poetry.group.dev.dependencies]
ruff = ">=0.5.1,<0.11.3"
ruff = ">=0.5.1,<0.9.10"
pytest = "^8.3.4"
mypy = "^1.14.1"
types-requests = "^2.32.0.20241016"
@ -40,7 +40,7 @@ pylint-pydantic = "^0.3.5"
coverage = "^7.6.10"
mkdocs = "^1.6.1"
mkdocs-material = "^9.6.1"
mkdocstrings = ">=0.27,<0.30"
mkdocstrings = ">=0.27,<0.29"
mkdocstrings-python = "^1.13.0"
pook = "^2.1.3"

View file

@ -21,8 +21,7 @@ sudo apt-get install -y \
libsystemd-dev \
libudev-dev \
pkg-config \
ripgrep \
lld
ripgrep
export PATH="$HOME/.cargo/bin:$PATH"
@ -37,7 +36,7 @@ sudo chgrp vscode ~/ -R
# shellcheck disable=SC1091
source scripts/devcontainer_poststart.sh
cargo install \
cargo install
cargo-audit \
mdbook-mermaid \
mdbook

View file

@ -25,7 +25,7 @@ def recover_account(username: str) -> str:
"recover-account",
username,
"--config",
"./insecure_server.toml",
"../../examples/insecure_server.toml",
"--output",
"json",
]

View file

@ -44,7 +44,7 @@ fi
# defaults
KANIDM_CONFIG_FILE="./insecure_server.toml"
KANIDM_CONFIG_FILE="../../examples/insecure_server.toml"
KANIDM_URL="$(rg origin "${KANIDM_CONFIG_FILE}" | awk '{print $NF}' | tr -d '"')"
KANIDM_CA_PATH="/tmp/kanidm/ca.pem"
@ -83,7 +83,7 @@ if [ "${REMOVE_TEST_DB}" -eq 1 ]; then
rm /tmp/kanidm/kanidm.db || true
fi
export KANIDM_CONFIG="./insecure_server.toml"
export KANIDM_CONFIG="../../examples/insecure_server.toml"
IDM_ADMIN_USER="idm_admin@localhost"
echo "Resetting the idm_admin user..."

View file

@ -25,7 +25,7 @@ if [ ! -f "run_insecure_dev_server.sh" ]; then
exit 1
fi
export KANIDM_CONFIG="./insecure_server.toml"
export KANIDM_CONFIG="../../examples/insecure_server.toml"
mkdir -p /tmp/kanidm/client_ca
@ -48,7 +48,7 @@ fi
ATTEMPT=0
KANIDM_CONFIG_FILE="./insecure_server.toml"
KANIDM_CONFIG_FILE="../../examples/insecure_server.toml"
KANIDM_URL="$(rg origin "${KANIDM_CONFIG_FILE}" | awk '{print $NF}' | tr -d '"')"
KANIDM_CA_PATH="/tmp/kanidm/ca.pem"

View file

@ -191,7 +191,7 @@ impl QueryServerReadV1 {
pub async fn handle_online_backup(
&self,
msg: OnlineBackupEvent,
outpath: &Path,
outpath: &str,
versions: usize,
) -> Result<(), OperationError> {
trace!(eventid = ?msg.eventid, "Begin online backup event");
@ -200,12 +200,12 @@ impl QueryServerReadV1 {
#[allow(clippy::unwrap_used)]
let timestamp = now.format(&Rfc3339).unwrap();
let dest_file = outpath.join(format!("backup-{}.json", timestamp));
let dest_file = format!("{}/backup-{}.json", outpath, timestamp);
if dest_file.exists() {
if Path::new(&dest_file).exists() {
error!(
"Online backup file {} already exists, will not overwrite it.",
dest_file.display()
dest_file
);
return Err(OperationError::InvalidState);
}
@ -218,14 +218,10 @@ impl QueryServerReadV1 {
.get_be_txn()
.backup(&dest_file)
.map(|()| {
info!("Online backup created {} successfully", dest_file.display());
info!("Online backup created {} successfully", dest_file);
})
.map_err(|e| {
error!(
"Online backup failed to create {}: {:?}",
dest_file.display(),
e
);
error!("Online backup failed to create {}: {:?}", dest_file, e);
OperationError::InvalidState
})?;
}
@ -271,11 +267,7 @@ impl QueryServerReadV1 {
}
}
Err(e) => {
error!(
"Online backup cleanup error read dir {}: {}",
outpath.display(),
e
);
error!("Online backup cleanup error read dir {}: {}", outpath, e);
return Err(OperationError::InvalidState);
}
}

File diff suppressed because it is too large Load diff

View file

@ -112,19 +112,19 @@ impl IntervalActor {
if !op.exists() {
info!(
"Online backup output folder '{}' does not exist, trying to create it.",
outpath.display()
outpath
);
fs::create_dir_all(&outpath).map_err(|e| {
error!(
"Online backup failed to create output directory '{}': {}",
outpath.display(),
outpath.clone(),
e
)
})?;
}
if !op.is_dir() {
error!("Online backup output '{}' is not a directory or we are missing permissions to access it.", outpath.display());
error!("Online backup output '{}' is not a directory or we are missing permissions to access it.", outpath);
return Err(());
}
@ -148,7 +148,7 @@ impl IntervalActor {
if let Err(e) = server
.handle_online_backup(
OnlineBackupEvent::new(),
&outpath,
outpath.clone().as_str(),
versions,
)
.await

View file

@ -1,5 +1,8 @@
use std::net;
use std::pin::Pin;
use std::str::FromStr;
use crate::actors::QueryServerReadV1;
use crate::CoreAction;
use futures_util::sink::SinkExt;
use futures_util::stream::StreamExt;
use kanidmd_lib::idm::ldap::{LdapBoundToken, LdapResponseState};
@ -7,16 +10,14 @@ use kanidmd_lib::prelude::*;
use ldap3_proto::proto::LdapMsg;
use ldap3_proto::LdapCodec;
use openssl::ssl::{Ssl, SslAcceptor};
use std::net;
use std::pin::Pin;
use std::str::FromStr;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::broadcast;
use tokio::sync::mpsc;
use tokio_openssl::SslStream;
use tokio_util::codec::{FramedRead, FramedWrite};
use crate::CoreAction;
use tokio::sync::broadcast;
use tokio::sync::mpsc;
struct LdapSession {
uat: Option<LdapBoundToken>,
}
@ -48,14 +49,28 @@ async fn client_process_msg(
.await
}
async fn client_process<STREAM>(
stream: STREAM,
async fn client_process(
tcpstream: TcpStream,
tls_acceptor: SslAcceptor,
client_address: net::SocketAddr,
qe_r_ref: &'static QueryServerReadV1,
) where
STREAM: AsyncRead + AsyncWrite,
{
let (r, w) = tokio::io::split(stream);
) {
// Start the event
// From the parameters we need to create an SslContext.
let mut tlsstream = match Ssl::new(tls_acceptor.context())
.and_then(|tls_obj| SslStream::new(tls_obj, tcpstream))
{
Ok(ta) => ta,
Err(e) => {
error!("LDAP TLS setup error, continuing -> {:?}", e);
return;
}
};
if let Err(e) = SslStream::accept(Pin::new(&mut tlsstream)).await {
error!("LDAP TLS accept error, continuing -> {:?}", e);
return;
};
let (r, w) = tokio::io::split(tlsstream);
let mut r = FramedRead::new(r, LdapCodec::default());
let mut w = FramedWrite::new(w, LdapCodec::default());
@ -111,32 +126,7 @@ async fn client_process<STREAM>(
}
}
async fn client_tls_accept(
tcpstream: TcpStream,
tls_acceptor: SslAcceptor,
client_socket_addr: net::SocketAddr,
qe_r_ref: &'static QueryServerReadV1,
) {
// Start the event
// From the parameters we need to create an SslContext.
let mut tlsstream = match Ssl::new(tls_acceptor.context())
.and_then(|tls_obj| SslStream::new(tls_obj, tcpstream))
{
Ok(ta) => ta,
Err(err) => {
error!(?err, %client_socket_addr, "LDAP TLS setup error");
return;
}
};
if let Err(err) = SslStream::accept(Pin::new(&mut tlsstream)).await {
error!(?err, %client_socket_addr, "LDAP TLS accept error");
return;
};
tokio::spawn(client_process(tlsstream, client_socket_addr, qe_r_ref));
}
/// TLS LDAP Listener, hands off to [client_tls_accept]
/// TLS LDAP Listener, hands off to [client_process]
async fn ldap_tls_acceptor(
listener: TcpListener,
mut tls_acceptor: SslAcceptor,
@ -155,10 +145,10 @@ async fn ldap_tls_acceptor(
match accept_result {
Ok((tcpstream, client_socket_addr)) => {
let clone_tls_acceptor = tls_acceptor.clone();
tokio::spawn(client_tls_accept(tcpstream, clone_tls_acceptor, client_socket_addr, qe_r_ref));
tokio::spawn(client_process(tcpstream, clone_tls_acceptor, client_socket_addr, qe_r_ref));
}
Err(err) => {
warn!(?err, "LDAP acceptor error, continuing");
Err(e) => {
error!("LDAP acceptor error, continuing -> {:?}", e);
}
}
}
@ -171,34 +161,6 @@ async fn ldap_tls_acceptor(
info!("Stopped {}", super::TaskName::LdapActor);
}
/// PLAIN LDAP Listener, hands off to [client_process]
async fn ldap_plaintext_acceptor(
listener: TcpListener,
qe_r_ref: &'static QueryServerReadV1,
mut rx: broadcast::Receiver<CoreAction>,
) {
loop {
tokio::select! {
Ok(action) = rx.recv() => {
match action {
CoreAction::Shutdown => break,
}
}
accept_result = listener.accept() => {
match accept_result {
Ok((tcpstream, client_socket_addr)) => {
tokio::spawn(client_process(tcpstream, client_socket_addr, qe_r_ref));
}
Err(e) => {
error!("LDAP acceptor error, continuing -> {:?}", e);
}
}
}
}
}
info!("Stopped {}", super::TaskName::LdapActor);
}
pub(crate) async fn create_ldap_server(
address: &str,
opt_ssl_acceptor: Option<SslAcceptor>,
@ -235,7 +197,10 @@ pub(crate) async fn create_ldap_server(
tls_acceptor_reload_rx,
))
}
None => tokio::spawn(ldap_plaintext_acceptor(listener, qe_r_ref, rx)),
None => {
error!("The server won't run without TLS!");
return Err(());
}
};
info!("Created LDAP interface");

View file

@ -36,10 +36,9 @@ mod ldaps;
mod repl;
mod utils;
use crate::actors::{QueryServerReadV1, QueryServerWriteV1};
use crate::admin::AdminActor;
use crate::config::{Configuration, ServerRole};
use crate::interval::IntervalActor;
use std::fmt::{Display, Formatter};
use std::sync::Arc;
use crate::utils::touch_file_or_quit;
use compact_jwt::{JwsHs256Signer, JwsSigner};
use kanidm_proto::internal::OperationError;
@ -51,14 +50,17 @@ use kanidmd_lib::status::StatusActor;
use kanidmd_lib::value::CredentialType;
#[cfg(not(target_family = "windows"))]
use libc::umask;
use std::fmt::{Display, Formatter};
use std::path::Path;
use std::sync::Arc;
use tokio::sync::broadcast;
use tokio::sync::mpsc;
use tokio::sync::Notify;
use tokio::task;
use crate::actors::{QueryServerReadV1, QueryServerWriteV1};
use crate::admin::AdminActor;
use crate::config::{Configuration, ServerRole};
use crate::interval::IntervalActor;
use tokio::sync::mpsc;
// === internal setup helpers
fn setup_backend(config: &Configuration, schema: &Schema) -> Result<Backend, OperationError> {
@ -78,7 +80,7 @@ fn setup_backend_vacuum(
let pool_size: u32 = config.threads as u32;
let cfg = BackendConfig::new(
config.db_path.as_deref(),
config.db_path.as_str(),
pool_size,
config.db_fs_type.unwrap_or_default(),
config.db_arc_size,
@ -333,7 +335,7 @@ pub fn dbscan_restore_quarantined_core(config: &Configuration, id: u64) {
};
}
pub fn backup_server_core(config: &Configuration, dst_path: &Path) {
pub fn backup_server_core(config: &Configuration, dst_path: &str) {
let schema = match Schema::new() {
Ok(s) => s,
Err(e) => {
@ -369,11 +371,8 @@ pub fn backup_server_core(config: &Configuration, dst_path: &Path) {
// Let the txn abort, even on success.
}
pub async fn restore_server_core(config: &Configuration, dst_path: &Path) {
// If it's an in memory database, we don't need to touch anything
if let Some(db_path) = config.db_path.as_ref() {
touch_file_or_quit(db_path);
}
pub async fn restore_server_core(config: &Configuration, dst_path: &str) {
touch_file_or_quit(config.db_path.as_str());
// First, we provide the in-memory schema so that core attrs are indexed correctly.
let schema = match Schema::new() {
@ -1012,7 +1011,7 @@ pub async fn create_server_core(
let tls_accepter_reload_task_notify = tls_acceptor_reload_notify.clone();
let tls_config = config.tls_config.clone();
let ldap_configured = config.ldapbindaddress.is_some();
let ldap_configured = config.ldapaddress.is_some();
let (ldap_tls_acceptor_reload_tx, ldap_tls_acceptor_reload_rx) = mpsc::channel(1);
let (http_tls_acceptor_reload_tx, http_tls_acceptor_reload_rx) = mpsc::channel(1);
@ -1077,19 +1076,24 @@ pub async fn create_server_core(
};
// If we have been requested to init LDAP, configure it now.
let maybe_ldap_acceptor_handle = match &config.ldapbindaddress {
let maybe_ldap_acceptor_handle = match &config.ldapaddress {
Some(la) => {
let opt_ldap_ssl_acceptor = maybe_tls_acceptor.clone();
let h = ldaps::create_ldap_server(
la.as_str(),
opt_ldap_ssl_acceptor,
server_read_ref,
broadcast_tx.subscribe(),
ldap_tls_acceptor_reload_rx,
)
.await?;
Some(h)
if !config_test {
// ⚠️ only start the sockets and listeners in non-config-test modes.
let h = ldaps::create_ldap_server(
la.as_str(),
opt_ldap_ssl_acceptor,
server_read_ref,
broadcast_tx.subscribe(),
ldap_tls_acceptor_reload_rx,
)
.await?;
Some(h)
} else {
None
}
}
None => {
debug!("LDAP not requested, skipping");

View file

@ -1,39 +1,32 @@
use filetime::FileTime;
use std::fs::File;
use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
use std::time::SystemTime;
pub fn touch_file_or_quit<P: AsRef<Path>>(file_path: P) {
pub fn touch_file_or_quit(file_path: &str) {
/*
Attempt to touch the file file_path, will quit the application if it fails for any reason.
Will also create a new file if it doesn't already exist.
*/
let file_path: &Path = file_path.as_ref();
if file_path.exists() {
if PathBuf::from(file_path).exists() {
let t = FileTime::from_system_time(SystemTime::now());
match filetime::set_file_times(file_path, t, t) {
Ok(_) => debug!(
"Successfully touched existing file {}, can continue",
file_path.display()
file_path
),
Err(e) => {
match e.kind() {
ErrorKind::PermissionDenied => {
// we bail here because you won't be able to write them back...
error!(
"Permission denied writing to {}, quitting.",
file_path.display()
)
error!("Permission denied writing to {}, quitting.", file_path)
}
_ => {
error!(
"Failed to write to {} due to error: {:?} ... quitting.",
file_path.display(),
e
file_path, e
)
}
}
@ -42,12 +35,11 @@ pub fn touch_file_or_quit<P: AsRef<Path>>(file_path: P) {
}
} else {
match File::create(file_path) {
Ok(_) => debug!("Successfully touched new file {}", file_path.display()),
Ok(_) => debug!("Successfully touched new file {}", file_path),
Err(e) => {
error!(
"Failed to write to {} due to error: {:?} ... quitting.",
file_path.display(),
e
file_path, e
);
std::process::exit(1);
}

View file

@ -57,31 +57,6 @@ clap = { workspace = true, features = ["derive"] }
clap_complete = { workspace = true }
kanidm_build_profiles = { workspace = true }
## Debian packaging
[package.metadata.deb]
name = "kanidmd"
maintainer = "James Hodgkinson <james@terminaloutcomes.com>"
# Can't use $auto depends because the name of libssl3 varies by distro and version
depends = [
"libc6",
"tpm-udev",
"libssl3 | libssl3t64",
]
section = "network"
priority = "optional"
changelog = "../../target/debian/changelog" # Generated by platform/debian/build_debs.sh
assets = [
[ "target/release/kanidmd", "usr/bin/", "755" ],
[ "debian/group.conf", "usr/lib/sysusers.d/kandimd.conf", "644" ],
[ "debian/server.toml", "etc/kanidmd/server.toml", "640" ],
[ "../../examples/server.toml", "usr/share/kanidmd/", "444" ],
[ "../core/static/**/*", "usr/share/kanidmd/static", "444" ],
]
maintainer-scripts = "debian/"
systemd-units = [
{ unit-name = "kanidmd", enable = false}, # Cannot start without manual config
]
[package.metadata.cargo-machete]
ignored = ["clap_complete", "kanidm_build_profiles"]

View file

@ -10,15 +10,13 @@ Before=radiusd.service
[Service]
Type=notify
DynamicUser=yes
User=kanidmd_dyn
Group=kanidmd
StateDirectory=kanidmd
StateDirectory=kanidm
StateDirectoryMode=0750
CacheDirectory=kanidmd
CacheDirectoryMode=0750
RuntimeDirectory=kanidmd
RuntimeDirectoryMode=0755
ExecStart=/usr/bin/kanidmd server
ExecStart=/usr/sbin/kanidmd server -c /etc/kanidm/server.toml
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE

View file

@ -1,2 +0,0 @@
# This is a sysusers.d format config, please refer to man sysusers.d(5)
g kanidmd -

View file

@ -1,38 +0,0 @@
#!/bin/sh
# postinst script for kanidmd
#
# see: dh_installdeb(1)
set -e
case "$1" in
configure)
echo "Creating the kanidmd group for config & cert ownership..."
systemd-sysusers
echo "Fixing ownership of server configuration ..."
chown :kanidmd /etc/kanidmd/server.toml*
echo "============================="
echo "Thanks for installing Kanidm!"
echo "============================="
echo "Please ensure you modify the configuration file at /etc/kanidmd/server.toml"
echo "Only then: systemctl enable kanidmd.service"
echo "Full examples are in /usr/share/kanidmd/"
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
# dh_installdeb will replace this with shell code automatically
# generated by other debhelper scripts.
#DEBHELPER#
exit 0

View file

@ -1,51 +0,0 @@
# Kanidm server minimal configuration - /etc/kanidm/server.toml
# For a full example and documentation, see /usr/share/kanidmd/server.toml
# or `example/server.toml` in the source repository
# NOTE: You must configure at least domain & origin below to allow the server to start!
# The webserver bind address. Requires TLS certificates.
# If the port is set to 443 you may require the
# NET_BIND_SERVICE capability.
# Defaults to "127.0.0.1:8443"
bindaddress = "127.0.0.1:8443"
# The path to the kanidm database.
# The provided example uses systemd dynamic user pathing for security
db_path = "/var/lib/private/kanidmd/kanidm.db"
# TLS chain and key in pem format. Both must be present.
# If the server receives a SIGHUP, these files will be
# re-read and reloaded if their content is valid.
# These should be owned by root:kanidmd to give the service access.
tls_chain = "/etc/kanidmd/chain.pem"
tls_key = "/etc/kanidmd/key.pem"
log_level = "info"
# The DNS domain name of the server. This is used in a
# number of security-critical contexts
# such as webauthn, so it *must* match your DNS
#
# ⚠️ WARNING ⚠️
#
# Changing this value after first use WILL break many types of
# registered credentials for accounts including but not limited
# to: webauthn, oauth tokens, and more.
# If you change this value you *must* run
# `kanidmd domain rename` immediately after.
# NOTE: You must set this value!
#domain = "idm.example.com"
#
# The origin for webauthn. This is the url to the server,
# with the port included if it is non-standard (any port
# except 443). This must match or be a descendent of the
# domain name you configure above. If these two items are
# not consistent, the server WILL refuse to start!
# origin = "https://idm.example.com"
# NOTE: You must set this value!
#origin = "https://idm.example.com:8443"
[online_backup]
path = "/var/lib/private/kanidmd/backups/"
schedule = "00 22 * * *"

View file

@ -22,16 +22,14 @@ fi
mkdir -p "${KANI_TMP}"/client_ca
CONFIG_FILE=${CONFIG_FILE:="${SCRIPT_DIR}/insecure_server.toml"}
CONFIG_FILE=${CONFIG_FILE:="${SCRIPT_DIR}/../../examples/insecure_server.toml"}
if [ ! -f "${CONFIG_FILE}" ]; then
echo "Couldn't find configuration file at ${CONFIG_FILE}, please ensure you're running this script from its base directory (${SCRIPT_DIR})."
exit 1
fi
# Save current directory and change to script directory without pushd
OLD_DIR=$(pwd)
cd "${SCRIPT_DIR}" || exit 1
pushd "${SCRIPT_DIR}" > /dev/null 2>&1
if [ -n "${1}" ]; then
COMMAND=$*
#shellcheck disable=SC2086
@ -42,4 +40,4 @@ else
#shellcheck disable=SC2086
cargo run ${KANI_CARGO_OPTS} --bin kanidmd -- server -c "${CONFIG_FILE}"
fi
cd "${OLD_DIR}" || exit 1
popd > /dev/null 2>&1

View file

@ -37,7 +37,7 @@ use kanidmd_core::admin::{
AdminTaskRequest, AdminTaskResponse, ClientCodec, ProtoDomainInfo,
ProtoDomainUpgradeCheckReport, ProtoDomainUpgradeCheckStatus,
};
use kanidmd_core::config::{CliConfig, Configuration, EnvironmentConfig, ServerConfigUntagged};
use kanidmd_core::config::{Configuration, ServerConfig};
use kanidmd_core::{
backup_server_core, cert_generate_core, create_server_core, dbscan_get_id2entry_core,
dbscan_list_id2entry_core, dbscan_list_index_analysis_core, dbscan_list_index_core,
@ -379,13 +379,17 @@ fn check_file_ownership(opt: &KanidmdParser) -> Result<(), ExitCode> {
}
// We have to do this because we can't use tracing until we've started the logging pipeline, and we can't start the logging pipeline until the tokio runtime's doing its thing.
async fn start_daemon(opt: KanidmdParser, config: Configuration) -> ExitCode {
async fn start_daemon(
opt: KanidmdParser,
mut config: Configuration,
sconfig: ServerConfig,
) -> ExitCode {
// if we have a server config and it has an OTEL URL, then we'll start the logging pipeline now.
// TODO: only send to stderr when we're not in a TTY
let sub = match sketching::otel::start_logging_pipeline(
&config.otel_grpc_url,
config.log_level,
&sconfig.otel_grpc_url,
sconfig.log_level.unwrap_or_default(),
"kanidmd",
) {
Err(err) => {
@ -419,8 +423,8 @@ async fn start_daemon(opt: KanidmdParser, config: Configuration) -> ExitCode {
return err;
};
if let Some(db_path) = config.db_path.as_ref() {
let db_pathbuf = db_path.to_path_buf();
if let Some(db_path) = sconfig.db_path.as_ref() {
let db_pathbuf = PathBuf::from(db_path.as_str());
// We can't check the db_path permissions because it may not exist yet!
if let Some(db_parent_path) = db_pathbuf.parent() {
if !db_parent_path.exists() {
@ -460,11 +464,33 @@ async fn start_daemon(opt: KanidmdParser, config: Configuration) -> ExitCode {
warn!("WARNING: DB folder {} has 'everyone' permission bits in the mode. This could be a security risk ...", db_par_path_buf.to_str().unwrap_or("invalid file path"));
}
}
config.update_db_path(db_path);
} else {
error!("No db_path set in configuration, server startup will FAIL!");
return ExitCode::FAILURE;
}
if let Some(origin) = sconfig.origin.clone() {
config.update_origin(&origin);
} else {
error!("No origin set in configuration, server startup will FAIL!");
return ExitCode::FAILURE;
}
if let Some(domain) = sconfig.domain.clone() {
config.update_domain(&domain);
} else {
error!("No domain set in configuration, server startup will FAIL!");
return ExitCode::FAILURE;
}
config.update_db_arc_size(sconfig.get_db_arc_size());
config.update_role(sconfig.role);
config.update_output_mode(opt.commands.commonopt().output_mode.to_owned().into());
config.update_trust_x_forward_for(sconfig.trust_x_forward_for);
config.update_admin_bind_path(&sconfig.adminbindpath);
config.update_replication_config(sconfig.repl_config.clone());
match &opt.commands {
// we aren't going to touch the DB so we can carry on
KanidmdOpt::ShowReplicationCertificate { .. }
@ -475,15 +501,19 @@ async fn start_daemon(opt: KanidmdParser, config: Configuration) -> ExitCode {
_ => {
// Okay - Lets now create our lock and go.
#[allow(clippy::expect_used)]
let klock_path = match config.db_path.clone() {
Some(val) => val.with_extension("klock"),
None => std::env::temp_dir().join("kanidmd.klock"),
let klock_path = match sconfig.db_path.clone() {
Some(val) => format!("{}.klock", val),
None => std::env::temp_dir()
.join("kanidmd.klock")
.to_str()
.expect("Unable to create klock path, this is a critical error!")
.to_string(),
};
let flock = match File::create(&klock_path) {
Ok(flock) => flock,
Err(e) => {
error!("ERROR: Refusing to start - unable to create kanidmd exclusive lock at {} - {:?}", klock_path.display(), e);
error!("ERROR: Refusing to start - unable to create kanidmd exclusive lock at {} - {:?}", klock_path, e);
return ExitCode::FAILURE;
}
};
@ -491,7 +521,7 @@ async fn start_daemon(opt: KanidmdParser, config: Configuration) -> ExitCode {
match flock.try_lock_exclusive() {
Ok(()) => debug!("Acquired kanidm exclusive lock"),
Err(e) => {
error!("ERROR: Refusing to start - unable to lock kanidmd exclusive lock at {} - {:?}", klock_path.display(), e);
error!("ERROR: Refusing to start - unable to lock kanidmd exclusive lock at {} - {:?}", klock_path, e);
error!("Is another kanidmd process running?");
return ExitCode::FAILURE;
}
@ -499,7 +529,7 @@ async fn start_daemon(opt: KanidmdParser, config: Configuration) -> ExitCode {
}
}
kanidm_main(config, opt).await
kanidm_main(sconfig, config, opt).await
}
fn main() -> ExitCode {
@ -526,6 +556,10 @@ fn main() -> ExitCode {
return ExitCode::SUCCESS;
};
//we set up a list of these so we can set the log config THEN log out the errors.
let mut config_error: Vec<String> = Vec::new();
let mut config = Configuration::new();
if env!("KANIDM_SERVER_CONFIG_PATH").is_empty() {
println!("CRITICAL: Kanidmd was not built correctly and is missing a valid KANIDM_SERVER_CONFIG_PATH value");
return ExitCode::FAILURE;
@ -547,56 +581,49 @@ fn main() -> ExitCode {
}
};
let maybe_sconfig = if let Some(config_path) = maybe_config_path {
match ServerConfigUntagged::new(config_path) {
Ok(c) => Some(c),
Err(err) => {
eprintln!("ERROR: Configuration Parse Failure: {:?}", err);
return ExitCode::FAILURE;
}
}
} else {
eprintln!("WARNING: No configuration path was provided, relying on environment variables.");
None
};
let envconfig = match EnvironmentConfig::new() {
Ok(ec) => ec,
Err(err) => {
eprintln!("ERROR: Environment Configuration Parse Failure: {:?}", err);
let sconfig = match ServerConfig::new(maybe_config_path) {
Ok(c) => Some(c),
Err(e) => {
config_error.push(format!("Config Parse failure {:?}", e));
return ExitCode::FAILURE;
}
};
let cli_config = CliConfig {
output_mode: Some(opt.commands.commonopt().output_mode.to_owned().into()),
};
let is_server = matches!(&opt.commands, KanidmdOpt::Server(_));
let config = Configuration::build()
.add_env_config(envconfig)
.add_opt_toml_config(maybe_sconfig)
// We always set threads to 1 unless it's the main server.
.add_cli_config(cli_config)
.is_server_mode(is_server)
.finish();
let Some(config) = config else {
eprintln!(
"ERROR: Unable to build server configuration from provided configuration inputs."
);
return ExitCode::FAILURE;
};
// ===========================================================================
// Config ready
// Get information on the windows username
#[cfg(target_family = "windows")]
get_user_details_windows();
if !config_error.is_empty() {
println!("There were errors on startup, which prevent the server from starting:");
for e in config_error {
println!(" - {}", e);
}
return ExitCode::FAILURE;
}
let sconfig = match sconfig {
Some(val) => val,
None => {
println!("Somehow you got an empty ServerConfig after error checking? Cannot start!");
return ExitCode::FAILURE;
}
};
// ===========================================================================
// Config ready
// We always set threads to 1 unless it's the main server.
if matches!(&opt.commands, KanidmdOpt::Server(_)) {
// If not updated, will default to maximum
if let Some(threads) = sconfig.thread_count {
config.update_threads_count(threads);
}
} else {
config.update_threads_count(1);
};
// Start the runtime
let maybe_rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(config.threads)
.enable_all()
@ -616,12 +643,16 @@ fn main() -> ExitCode {
}
};
rt.block_on(start_daemon(opt, config))
rt.block_on(start_daemon(opt, config, sconfig))
}
/// Build and execute the main server. The ServerConfig are the configuration options
/// that we are processing into the config for the main server.
async fn kanidm_main(config: Configuration, opt: KanidmdParser) -> ExitCode {
async fn kanidm_main(
sconfig: ServerConfig,
mut config: Configuration,
opt: KanidmdParser,
) -> ExitCode {
match &opt.commands {
KanidmdOpt::Server(_sopt) | KanidmdOpt::ConfigTest(_sopt) => {
let config_test = matches!(&opt.commands, KanidmdOpt::ConfigTest(_));
@ -631,90 +662,88 @@ async fn kanidm_main(config: Configuration, opt: KanidmdParser) -> ExitCode {
info!("Running in server mode ...");
};
// Verify the TLs configs.
if let Some(tls_config) = config.tls_config.as_ref() {
{
let i_meta = match metadata(&tls_config.chain) {
Ok(m) => m,
Err(e) => {
error!(
"Unable to read metadata for TLS chain file '{}' - {:?}",
tls_config.chain.display(),
e
);
let diag =
kanidm_lib_file_permissions::diagnose_path(&tls_config.chain);
info!(%diag);
return ExitCode::FAILURE;
}
};
if !kanidm_lib_file_permissions::readonly(&i_meta) {
warn!("permissions on {} may not be secure. Should be readonly to running uid. This could be a security risk ...", tls_config.chain.display());
}
}
// configuration options that only relate to server mode
config.update_config_for_server_mode(&sconfig);
{
let i_meta = match metadata(&tls_config.key) {
Ok(m) => m,
Err(e) => {
error!(
"Unable to read metadata for TLS key file '{}' - {:?}",
tls_config.key.display(),
e
);
let diag = kanidm_lib_file_permissions::diagnose_path(&tls_config.key);
info!(%diag);
return ExitCode::FAILURE;
}
};
if !kanidm_lib_file_permissions::readonly(&i_meta) {
warn!("permissions on {} may not be secure. Should be readonly to running uid. This could be a security risk ...", tls_config.key.display());
}
#[cfg(not(target_os = "windows"))]
if i_meta.mode() & 0o007 != 0 {
warn!("WARNING: {} has 'everyone' permission bits in the mode. This could be a security risk ...", tls_config.key.display());
}
}
if let Some(ca_dir) = tls_config.client_ca.as_ref() {
// check that the TLS client CA config option is what we expect
let ca_dir_path = PathBuf::from(&ca_dir);
if !ca_dir_path.exists() {
if let Some(i_str) = &(sconfig.tls_chain) {
let i_path = PathBuf::from(i_str.as_str());
let i_meta = match metadata(&i_path) {
Ok(m) => m,
Err(e) => {
error!(
"TLS CA folder {} does not exist, server startup will FAIL!",
ca_dir.display()
"Unable to read metadata for TLS chain file '{}' - {:?}",
&i_path.to_str().unwrap_or("invalid file path"),
e
);
let diag = kanidm_lib_file_permissions::diagnose_path(&ca_dir_path);
let diag = kanidm_lib_file_permissions::diagnose_path(&i_path);
info!(%diag);
}
let i_meta = match metadata(&ca_dir_path) {
Ok(m) => m,
Err(e) => {
error!(
"Unable to read metadata for '{}' - {:?}",
ca_dir.display(),
e
);
let diag = kanidm_lib_file_permissions::diagnose_path(&ca_dir_path);
info!(%diag);
return ExitCode::FAILURE;
}
};
if !i_meta.is_dir() {
error!(
"ERROR: Refusing to run - TLS Client CA folder {} may not be a directory",
ca_dir.display()
);
return ExitCode::FAILURE;
}
if kanidm_lib_file_permissions::readonly(&i_meta) {
warn!("WARNING: TLS Client CA folder permissions on {} indicate it may not be RW. This could cause the server start up to fail!", ca_dir.display());
};
if !kanidm_lib_file_permissions::readonly(&i_meta) {
warn!("permissions on {} may not be secure. Should be readonly to running uid. This could be a security risk ...", i_str);
}
}
if let Some(i_str) = &(sconfig.tls_key) {
let i_path = PathBuf::from(i_str.as_str());
let i_meta = match metadata(&i_path) {
Ok(m) => m,
Err(e) => {
error!(
"Unable to read metadata for TLS key file '{}' - {:?}",
&i_path.to_str().unwrap_or("invalid file path"),
e
);
let diag = kanidm_lib_file_permissions::diagnose_path(&i_path);
info!(%diag);
return ExitCode::FAILURE;
}
#[cfg(not(target_os = "windows"))]
if i_meta.mode() & 0o007 != 0 {
warn!("WARNING: TLS Client CA folder {} has 'everyone' permission bits in the mode. This could be a security risk ...", ca_dir.display());
};
if !kanidm_lib_file_permissions::readonly(&i_meta) {
warn!("permissions on {} may not be secure. Should be readonly to running uid. This could be a security risk ...", i_str);
}
#[cfg(not(target_os = "windows"))]
if i_meta.mode() & 0o007 != 0 {
warn!("WARNING: {} has 'everyone' permission bits in the mode. This could be a security risk ...", i_str);
}
}
if let Some(ca_dir) = &(sconfig.tls_client_ca) {
// check that the TLS client CA config option is what we expect
let ca_dir_path = PathBuf::from(&ca_dir);
if !ca_dir_path.exists() {
error!(
"TLS CA folder {} does not exist, server startup will FAIL!",
ca_dir
);
let diag = kanidm_lib_file_permissions::diagnose_path(&ca_dir_path);
info!(%diag);
}
let i_meta = match metadata(&ca_dir_path) {
Ok(m) => m,
Err(e) => {
error!("Unable to read metadata for '{}' - {:?}", ca_dir, e);
let diag = kanidm_lib_file_permissions::diagnose_path(&ca_dir_path);
info!(%diag);
return ExitCode::FAILURE;
}
};
if !i_meta.is_dir() {
error!(
"ERROR: Refusing to run - TLS Client CA folder {} may not be a directory",
ca_dir
);
return ExitCode::FAILURE;
}
if kanidm_lib_file_permissions::readonly(&i_meta) {
warn!("WARNING: TLS Client CA folder permissions on {} indicate it may not be RW. This could cause the server start up to fail!", ca_dir);
}
#[cfg(not(target_os = "windows"))]
if i_meta.mode() & 0o007 != 0 {
warn!("WARNING: TLS Client CA folder {} has 'everyone' permission bits in the mode. This could be a security risk ...", ca_dir);
}
}
@ -724,6 +753,14 @@ async fn kanidm_main(config: Configuration, opt: KanidmdParser) -> ExitCode {
#[cfg(target_os = "linux")]
{
let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Ready]);
// Undocumented systemd feature - all messages should have a monotonic usec sent
// with them. In some cases like "reloading" messages, it is undocumented but
// failure to send this message causes the reload to fail.
if let Ok(monotonic_usec) = sd_notify::NotifyState::monotonic_usec_now() {
let _ = sd_notify::notify(true, &[monotonic_usec]);
} else {
error!("CRITICAL!!! Unable to access clock monotonic time. SYSTEMD WILL KILL US.");
};
let _ = sd_notify::notify(
true,
&[sd_notify::NotifyState::Status("Started Kanidm 🦀")],
@ -737,80 +774,86 @@ async fn kanidm_main(config: Configuration, opt: KanidmdParser) -> ExitCode {
{
let mut listener = sctx.subscribe();
tokio::select! {
Ok(()) = tokio::signal::ctrl_c() => {
break
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::terminate();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
break
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::alarm();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::hangup();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Reload TLS certificates
// systemd has a special reload handler for this.
#[cfg(target_os = "linux")]
{
if let Ok(monotonic_usec) = sd_notify::NotifyState::monotonic_usec_now() {
let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Reloading, monotonic_usec]);
let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Status("Reloading ...")]);
} else {
error!("CRITICAL!!! Unable to access clock monotonic time. SYSTEMD WILL KILL US.");
};
}
Ok(()) = tokio::signal::ctrl_c() => {
break
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::terminate();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
break
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::alarm();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::hangup();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Reload TLS certificates
// systemd has a special reload handler for this.
#[cfg(target_os = "linux")]
{
let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Reloading]);
// CRITICAL - if you do not send a monotonic usec message after a reloading
// message, your service WILL BE KILLED.
if let Ok(monotonic_usec) = sd_notify::NotifyState::monotonic_usec_now() {
let _ =
sd_notify::notify(true, &[monotonic_usec]);
} else {
error!("CRITICAL!!! Unable to access clock monotonic time. SYSTEMD WILL KILL US.");
};
let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Status("Reloading ...")]);
}
sctx.tls_acceptor_reload().await;
sctx.tls_acceptor_reload().await;
// Systemd freaks out if you send the ready state too fast after the
// reload state and can kill Kanidmd as a result.
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
// Systemd freaks out if you send the ready state too fast after the
// reload state and can kill Kanidmd as a result.
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
#[cfg(target_os = "linux")]
{
if let Ok(monotonic_usec) = sd_notify::NotifyState::monotonic_usec_now() {
let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Ready, monotonic_usec]);
let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Status("Reload Success")]);
} else {
error!("CRITICAL!!! Unable to access clock monotonic time. SYSTEMD WILL KILL US.");
};
}
#[cfg(target_os = "linux")]
{
let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Ready]);
if let Ok(monotonic_usec) = sd_notify::NotifyState::monotonic_usec_now() {
let _ =
sd_notify::notify(true, &[monotonic_usec]);
} else {
error!("CRITICAL!!! Unable to access clock monotonic time. SYSTEMD WILL KILL US.");
};
let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Status("Reload Success")]);
}
info!("Reload complete");
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::user_defined1();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::user_defined2();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
}
// we got a message on thr broadcast from somewhere else
Ok(msg) = async move {
listener.recv().await
} => {
debug!("Main loop received message: {:?}", msg);
break
}
}
info!("Reload complete");
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::user_defined1();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::user_defined2();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
}
// we got a message on thr broadcast from somewhere else
Ok(msg) = async move {
listener.recv().await
} => {
debug!("Main loop received message: {:?}", msg);
break
}
}
}
#[cfg(target_family = "windows")]
{
@ -837,19 +880,34 @@ async fn kanidm_main(config: Configuration, opt: KanidmdParser) -> ExitCode {
}
KanidmdOpt::CertGenerate(_sopt) => {
info!("Running in certificate generate mode ...");
config.update_config_for_server_mode(&sconfig);
cert_generate_core(&config);
}
KanidmdOpt::Database {
commands: DbCommands::Backup(bopt),
} => {
info!("Running in backup mode ...");
backup_server_core(&config, &bopt.path);
let p = match bopt.path.to_str() {
Some(p) => p,
None => {
error!("Invalid backup path");
return ExitCode::FAILURE;
}
};
backup_server_core(&config, p);
}
KanidmdOpt::Database {
commands: DbCommands::Restore(ropt),
} => {
info!("Running in restore mode ...");
restore_server_core(&config, &ropt.path).await;
let p = match ropt.path.to_str() {
Some(p) => p,
None => {
error!("Invalid restore path");
return ExitCode::FAILURE;
}
};
restore_server_core(&config, p).await;
}
KanidmdOpt::Database {
commands: DbCommands::Verify(_vopt),
@ -1030,6 +1088,8 @@ async fn kanidm_main(config: Configuration, opt: KanidmdParser) -> ExitCode {
vacuum_server_core(&config);
}
KanidmdOpt::HealthCheck(sopt) => {
config.update_config_for_server_mode(&sconfig);
debug!("{sopt:?}");
let healthcheck_url = match &sopt.check_origin {
@ -1050,15 +1110,12 @@ async fn kanidm_main(config: Configuration, opt: KanidmdParser) -> ExitCode {
.danger_accept_invalid_hostnames(!sopt.verify_tls)
.https_only(true);
client = match &config.tls_config {
client = match &sconfig.tls_chain {
None => client,
Some(tls_config) => {
debug!(
"Trying to load {} to build a CA cert path",
tls_config.chain.display()
);
Some(ca_cert) => {
debug!("Trying to load {} to build a CA cert path", ca_cert);
// if the ca_cert file exists, then we'll use it
let ca_cert_path = tls_config.chain.clone();
let ca_cert_path = PathBuf::from(ca_cert);
match ca_cert_path.exists() {
true => {
let mut cert_buf = Vec::new();
@ -1091,10 +1148,7 @@ async fn kanidm_main(config: Configuration, opt: KanidmdParser) -> ExitCode {
client
}
false => {
warn!(
"Couldn't find ca cert {} but carrying on...",
tls_config.chain.display()
);
warn!("Couldn't find ca cert {} but carrying on...", ca_cert);
client
}
}

View file

@ -34,7 +34,7 @@ fn parse_attributes(
});
if !args_are_allowed {
let msg = "Invalid test config attribute. The following are allowed";
let msg = "Invalid test config attribute. The following are allow";
return Err(syn::Error::new_spanned(
input.sig.fn_token,
format!("{}: {}", msg, ALLOWED_ATTRIBUTES.join(", ")),

View file

@ -1,21 +1,27 @@
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::convert::{TryFrom, TryInto};
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use super::keystorage::{KeyHandle, KeyHandleId};
// use crate::valueset;
use hashbrown::HashMap;
use idlset::v2::IDLBitRange;
use kanidm_proto::internal::{ConsistencyError, OperationError};
use rusqlite::vtab::array::Array;
use rusqlite::{Connection, OpenFlags, OptionalExtension};
use uuid::Uuid;
use crate::be::dbentry::DbIdentSpn;
use crate::be::dbvalue::DbCidV1;
use crate::be::{BackendConfig, IdList, IdRawEntry, IdxKey, IdxSlope};
use crate::entry::{Entry, EntryCommitted, EntrySealed};
use crate::prelude::*;
use crate::value::{IndexType, Value};
use hashbrown::HashMap;
use idlset::v2::IDLBitRange;
use kanidm_proto::internal::{ConsistencyError, OperationError};
use rusqlite::vtab::array::Array;
use rusqlite::{Connection, OpenFlags, OptionalExtension};
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::convert::{TryFrom, TryInto};
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use uuid::Uuid;
// use uuid::Uuid;
const DBV_ID2ENTRY: &str = "id2entry";
const DBV_INDEXV: &str = "indexv";
@ -199,7 +205,7 @@ pub(crate) trait IdlSqliteTransaction {
let mut stmt = self
.get_conn()?
.prepare(&format!(
"SELECT rowid from {}.sqlite_master where type=\"table\" AND name = :tname LIMIT 1",
"SELECT rowid from {}.sqlite_master where name = :tname LIMIT 1",
self.get_db_name()
))
.map_err(sqlite_error)?;
@ -1706,7 +1712,7 @@ impl IdlSqliteWriteTransaction {
impl IdlSqlite {
pub fn new(cfg: &BackendConfig, vacuum: bool) -> Result<Self, OperationError> {
if cfg.path.as_os_str().is_empty() {
if cfg.path.is_empty() {
debug_assert_eq!(cfg.pool_size, 1);
}
// If provided, set the page size to match the tuning we want. By default we use 4096. The VACUUM
@ -1728,7 +1734,8 @@ impl IdlSqlite {
// Initial setup routines.
{
let vconn = Connection::open_with_flags(&cfg.path, flags).map_err(sqlite_error)?;
let vconn =
Connection::open_with_flags(cfg.path.as_str(), flags).map_err(sqlite_error)?;
vconn
.execute_batch(
@ -1757,7 +1764,8 @@ impl IdlSqlite {
);
*/
let vconn = Connection::open_with_flags(&cfg.path, flags).map_err(sqlite_error)?;
let vconn =
Connection::open_with_flags(cfg.path.as_str(), flags).map_err(sqlite_error)?;
vconn
.execute_batch("PRAGMA wal_checkpoint(TRUNCATE);")
@ -1778,7 +1786,8 @@ impl IdlSqlite {
OperationError::SqliteError
})?;
let vconn = Connection::open_with_flags(&cfg.path, flags).map_err(sqlite_error)?;
let vconn =
Connection::open_with_flags(cfg.path.as_str(), flags).map_err(sqlite_error)?;
vconn
.pragma_update(None, "page_size", cfg.fstype as u32)
@ -1812,7 +1821,7 @@ impl IdlSqlite {
.map(|i| {
trace!("Opening Connection {}", i);
let conn =
Connection::open_with_flags(&cfg.path, flags).map_err(sqlite_error);
Connection::open_with_flags(cfg.path.as_str(), flags).map_err(sqlite_error);
match conn {
Ok(conn) => {
// We need to set the cachesize at this point as well.

View file

@ -4,6 +4,20 @@
//! is to persist content safely to disk, load that content, and execute queries
//! utilising indexes in the most effective way possible.
use std::collections::BTreeMap;
use std::fs;
use std::ops::DerefMut;
use std::sync::Arc;
use std::time::Duration;
use concread::cowcell::*;
use hashbrown::{HashMap as Map, HashSet};
use idlset::v2::IDLBitRange;
use idlset::AndNot;
use kanidm_proto::internal::{ConsistencyError, OperationError};
use tracing::{trace, trace_span};
use uuid::Uuid;
use crate::be::dbentry::{DbBackup, DbEntry};
use crate::be::dbrepl::DbReplMeta;
use crate::entry::Entry;
@ -17,19 +31,6 @@ use crate::repl::ruv::{
};
use crate::utils::trigraph_iter;
use crate::value::{IndexType, Value};
use concread::cowcell::*;
use hashbrown::{HashMap as Map, HashSet};
use idlset::v2::IDLBitRange;
use idlset::AndNot;
use kanidm_proto::internal::{ConsistencyError, OperationError};
use std::collections::BTreeMap;
use std::fs;
use std::ops::DerefMut;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use tracing::{trace, trace_span};
use uuid::Uuid;
pub(crate) mod dbentry;
pub(crate) mod dbrepl;
@ -131,7 +132,7 @@ impl IdxMeta {
#[derive(Clone)]
pub struct BackendConfig {
path: PathBuf,
path: String,
pool_size: u32,
db_name: &'static str,
fstype: FsType,
@ -140,16 +141,10 @@ pub struct BackendConfig {
}
impl BackendConfig {
pub fn new(
path: Option<&Path>,
pool_size: u32,
fstype: FsType,
arcsize: Option<usize>,
) -> Self {
pub fn new(path: &str, pool_size: u32, fstype: FsType, arcsize: Option<usize>) -> Self {
BackendConfig {
pool_size,
// This means if path is None, that "" implies an sqlite in memory/ram only database.
path: path.unwrap_or_else(|| Path::new("")).to_path_buf(),
path: path.to_string(),
db_name: "main",
fstype,
arcsize,
@ -159,7 +154,7 @@ impl BackendConfig {
pub(crate) fn new_test(db_name: &'static str) -> Self {
BackendConfig {
pool_size: 1,
path: PathBuf::from(""),
path: "".to_string(),
db_name,
fstype: FsType::Generic,
arcsize: Some(2048),
@ -554,11 +549,10 @@ pub trait BackendTransaction {
}
(_, fp) => {
plan.push(fp);
let setplan = FilterPlan::InclusionInvalid(plan);
error!(
?setplan,
filter_error!(
"Inclusion is unable to proceed - all terms must be fully indexed!"
);
let setplan = FilterPlan::InclusionInvalid(plan);
return Ok((IdList::Partial(IDLBitRange::new()), setplan));
}
}
@ -941,7 +935,7 @@ pub trait BackendTransaction {
self.get_ruv().verify(&entries, results);
}
fn backup(&mut self, dst_path: &Path) -> Result<(), OperationError> {
fn backup(&mut self, dst_path: &str) -> Result<(), OperationError> {
let repl_meta = self.get_ruv().to_db_backup_ruv();
// load all entries into RAM, may need to change this later
@ -1433,16 +1427,20 @@ impl<'a> BackendWriteTransaction<'a> {
if self.is_idx_slopeyness_generated()? {
trace!("Indexing slopes available");
} else {
warn!("No indexing slopes available. You should consider reindexing to generate these");
admin_warn!(
"No indexing slopes available. You should consider reindexing to generate these"
);
};
// Setup idxkeys here. By default we set these all to "max slope" aka
// all indexes are "equal" but also worse case unless analysed. If they
// have been analysed, we can set the slope factor into here.
let mut idxkeys = idxkeys
let idxkeys: Result<Map<_, _>, _> = idxkeys
.into_iter()
.map(|k| self.get_idx_slope(&k).map(|slope| (k, slope)))
.collect::<Result<Map<_, _>, _>>()?;
.collect();
let mut idxkeys = idxkeys?;
std::mem::swap(&mut self.idxmeta_wr.deref_mut().idxkeys, &mut idxkeys);
Ok(())
@ -1813,7 +1811,7 @@ impl<'a> BackendWriteTransaction<'a> {
Ok(slope)
}
pub fn restore(&mut self, src_path: &Path) -> Result<(), OperationError> {
pub fn restore(&mut self, src_path: &str) -> Result<(), OperationError> {
let serialized_string = fs::read_to_string(src_path).map_err(|e| {
admin_error!("fs::read_to_string {:?}", e);
OperationError::FsError
@ -2126,7 +2124,7 @@ impl Backend {
debug!(db_tickets = ?cfg.pool_size, profile = %env!("KANIDM_PROFILE_NAME"), cpu_flags = %env!("KANIDM_CPU_FLAGS"));
// If in memory, reduce pool to 1
if cfg.path.as_os_str().is_empty() {
if cfg.path.is_empty() {
cfg.pool_size = 1;
}
@ -2212,6 +2210,13 @@ impl Backend {
#[cfg(test)]
mod tests {
use std::fs;
use std::iter::FromIterator;
use std::sync::Arc;
use std::time::Duration;
use idlset::v2::IDLBitRange;
use super::super::entry::{Entry, EntryInit, EntryNew};
use super::Limits;
use super::{
@ -2221,12 +2226,6 @@ mod tests {
use crate::prelude::*;
use crate::repl::cid::Cid;
use crate::value::{IndexType, PartialValue, Value};
use idlset::v2::IDLBitRange;
use std::fs;
use std::iter::FromIterator;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
lazy_static! {
static ref CID_ZERO: Cid = Cid::new_zero();
@ -2601,9 +2600,11 @@ mod tests {
#[test]
fn test_be_backup_restore() {
let db_backup_file_name =
Path::new(option_env!("OUT_DIR").unwrap_or("/tmp")).join(".backup_test.json");
eprintln!(" ⚠️ {}", db_backup_file_name.display());
let db_backup_file_name = format!(
"{}/.backup_test.json",
option_env!("OUT_DIR").unwrap_or("/tmp")
);
eprintln!(" ⚠️ {db_backup_file_name}");
run_test!(|be: &mut BackendWriteTransaction| {
// Important! Need db metadata setup!
be.reset_db_s_uuid().unwrap();
@ -2658,9 +2659,11 @@ mod tests {
#[test]
fn test_be_backup_restore_tampered() {
let db_backup_file_name =
Path::new(option_env!("OUT_DIR").unwrap_or("/tmp")).join(".backup2_test.json");
eprintln!(" ⚠️ {}", db_backup_file_name.display());
let db_backup_file_name = format!(
"{}/.backup2_test.json",
option_env!("OUT_DIR").unwrap_or("/tmp")
);
eprintln!(" ⚠️ {db_backup_file_name}");
run_test!(|be: &mut BackendWriteTransaction| {
// Important! Need db metadata setup!
be.reset_db_s_uuid().unwrap();

View file

@ -136,6 +136,8 @@ pub const UUID_SCHEMA_ATTR_PRIMARY_CREDENTIAL: Uuid = uuid!("00000000-0000-0000-
pub const UUID_SCHEMA_CLASS_PERSON: Uuid = uuid!("00000000-0000-0000-0000-ffff00000044");
pub const UUID_SCHEMA_CLASS_GROUP: Uuid = uuid!("00000000-0000-0000-0000-ffff00000045");
pub const UUID_SCHEMA_CLASS_ACCOUNT: Uuid = uuid!("00000000-0000-0000-0000-ffff00000046");
pub const UUID_SCHEMA_ATTR_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000187");
pub const UUID_SCHEMA_ATTR_ATTRIBUTENAME: Uuid = uuid!("00000000-0000-0000-0000-ffff00000048");
pub const UUID_SCHEMA_ATTR_CLASSNAME: Uuid = uuid!("00000000-0000-0000-0000-ffff00000049");
pub const UUID_SCHEMA_ATTR_LEGALNAME: Uuid = uuid!("00000000-0000-0000-0000-ffff00000050");
@ -327,13 +329,6 @@ pub const UUID_SCHEMA_ATTR_ALLOW_PRIMARY_CRED_FALLBACK: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000185");
pub const UUID_SCHEMA_ATTR_DOMAIN_ALLOW_EASTER_EGGS: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000186");
pub const UUID_SCHEMA_ATTR_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000187");
pub const UUID_SCHEMA_ATTR_INDEXED: Uuid = uuid!("00000000-0000-0000-0000-ffff00000188");
pub const UUID_SCHEMA_ATTR_ACP_MODIFY_PRESENT_CLASS: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000189");
pub const UUID_SCHEMA_ATTR_ACP_MODIFY_REMOVE_CLASS: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000190");
// System and domain infos
// I'd like to strongly criticise william of the past for making poor choices about these allocations.

View file

@ -492,97 +492,6 @@ impl Entry<EntryInit, EntryNew> {
}
}
impl From<SchemaAttribute> for EntryInitNew {
fn from(value: SchemaAttribute) -> Self {
EntryInitNew::from(&value)
}
}
impl From<&SchemaAttribute> for EntryInitNew {
fn from(s: &SchemaAttribute) -> Self {
// Build the Map of the attributes
let mut attrs = Eattrs::new();
attrs.insert(Attribute::AttributeName, vs_iutf8![s.name.as_str()]);
attrs.insert(Attribute::Description, vs_utf8![s.description.to_owned()]);
attrs.insert(Attribute::Uuid, vs_uuid![s.uuid]);
attrs.insert(Attribute::MultiValue, vs_bool![s.multivalue]);
attrs.insert(Attribute::Phantom, vs_bool![s.phantom]);
attrs.insert(Attribute::SyncAllowed, vs_bool![s.sync_allowed]);
attrs.insert(Attribute::Replicated, vs_bool![s.replicated.into()]);
attrs.insert(Attribute::Unique, vs_bool![s.unique]);
attrs.insert(Attribute::Indexed, vs_bool![s.indexed]);
attrs.insert(Attribute::Syntax, vs_syntax![s.syntax]);
attrs.insert(
Attribute::Class,
vs_iutf8![
EntryClass::Object.into(),
EntryClass::System.into(),
EntryClass::AttributeType.into()
],
);
// Insert stuff.
Entry {
valid: EntryInit,
state: EntryNew,
attrs,
}
}
}
impl From<SchemaClass> for EntryInitNew {
fn from(value: SchemaClass) -> Self {
EntryInitNew::from(&value)
}
}
impl From<&SchemaClass> for EntryInitNew {
fn from(s: &SchemaClass) -> Self {
let mut attrs = Eattrs::new();
attrs.insert(Attribute::ClassName, vs_iutf8![s.name.as_str()]);
attrs.insert(Attribute::Description, vs_utf8![s.description.to_owned()]);
attrs.insert(Attribute::SyncAllowed, vs_bool![s.sync_allowed]);
attrs.insert(Attribute::Uuid, vs_uuid![s.uuid]);
attrs.insert(
Attribute::Class,
vs_iutf8![
EntryClass::Object.into(),
EntryClass::System.into(),
EntryClass::ClassType.into()
],
);
let vs_systemmay = ValueSetIutf8::from_iter(s.systemmay.iter().map(|sm| sm.as_str()));
if let Some(vs) = vs_systemmay {
attrs.insert(Attribute::SystemMay, vs);
}
let vs_systemmust = ValueSetIutf8::from_iter(s.systemmust.iter().map(|sm| sm.as_str()));
if let Some(vs) = vs_systemmust {
attrs.insert(Attribute::SystemMust, vs);
}
let vs_systemexcludes =
ValueSetIutf8::from_iter(s.systemexcludes.iter().map(|sm| sm.as_str()));
if let Some(vs) = vs_systemexcludes {
attrs.insert(Attribute::SystemExcludes, vs);
}
let vs_systemsupplements =
ValueSetIutf8::from_iter(s.systemsupplements.iter().map(|sm| sm.as_str()));
if let Some(vs) = vs_systemsupplements {
attrs.insert(Attribute::SystemSupplements, vs);
}
Entry {
valid: EntryInit,
state: EntryNew,
attrs,
}
}
}
impl Entry<EntryRefresh, EntryNew> {
pub fn from_repl_entry_v1(repl_entry: ReplEntryV1) -> Result<Self, OperationError> {
// From the entry, we have to rebuild the ecstate and the attrs.
@ -2040,7 +1949,7 @@ impl<STATE> Entry<EntryValid, STATE> {
};
if !valid_supplements {
warn!(
admin_warn!(
"Validation error, the following possible supplement classes are missing - {:?}",
supplements_classes
);
@ -2724,6 +2633,21 @@ impl<VALID, STATE> Entry<VALID, STATE> {
// These are special types to allow returning typed values from
// an entry, if we "know" what we expect to receive.
/// This returns an array of IndexTypes, when the type is an Optional
/// multivalue in schema - IE this will *not* fail if the attribute is
/// empty, yielding and empty array instead.
///
/// However, the conversion to IndexType is fallible, so in case of a failure
/// to convert, an empty vec is returned
pub(crate) fn get_ava_opt_index<A: AsRef<Attribute>>(&self, attr: A) -> Option<Vec<IndexType>> {
if let Some(vs) = self.get_ava_set(attr) {
vs.as_indextype_iter().map(|i| i.collect())
} else {
// Empty, but consider as valid.
Some(vec![])
}
}
/// Return a single value of this attributes name, or `None` if it is NOT present, or
/// there are multiple values present (ambiguous).
pub fn get_ava_single<A: AsRef<Attribute>>(&self, attr: A) -> Option<Value> {
@ -3336,6 +3260,97 @@ impl<VALID, STATE> PartialEq for Entry<VALID, STATE> {
}
}
impl From<&SchemaAttribute> for Entry<EntryInit, EntryNew> {
fn from(s: &SchemaAttribute) -> Self {
// Convert an Attribute to an entry ... make it good!
let uuid_v = vs_uuid![s.uuid];
let name_v = vs_iutf8![s.name.as_str()];
let desc_v = vs_utf8![s.description.to_owned()];
let multivalue_v = vs_bool![s.multivalue];
let sync_allowed_v = vs_bool![s.sync_allowed];
let replicated_v = vs_bool![s.replicated];
let phantom_v = vs_bool![s.phantom];
let unique_v = vs_bool![s.unique];
let index_v = ValueSetIndex::from_iter(s.index.iter().copied());
let syntax_v = vs_syntax![s.syntax];
// Build the Map of the attributes relevant
// let mut attrs: Map<AttrString, Set<Value>> = Map::with_capacity(8);
let mut attrs: Map<Attribute, ValueSet> = Map::new();
attrs.insert(Attribute::AttributeName, name_v);
attrs.insert(Attribute::Description, desc_v);
attrs.insert(Attribute::Uuid, uuid_v);
attrs.insert(Attribute::MultiValue, multivalue_v);
attrs.insert(Attribute::Phantom, phantom_v);
attrs.insert(Attribute::SyncAllowed, sync_allowed_v);
attrs.insert(Attribute::Replicated, replicated_v);
attrs.insert(Attribute::Unique, unique_v);
if let Some(vs) = index_v {
attrs.insert(Attribute::Index, vs);
}
attrs.insert(Attribute::Syntax, syntax_v);
attrs.insert(
Attribute::Class,
vs_iutf8![
EntryClass::Object.into(),
EntryClass::System.into(),
EntryClass::AttributeType.into()
],
);
// Insert stuff.
Entry {
valid: EntryInit,
state: EntryNew,
attrs,
}
}
}
impl From<&SchemaClass> for Entry<EntryInit, EntryNew> {
fn from(s: &SchemaClass) -> Self {
let uuid_v = vs_uuid![s.uuid];
let name_v = vs_iutf8![s.name.as_str()];
let desc_v = vs_utf8![s.description.to_owned()];
let sync_allowed_v = vs_bool![s.sync_allowed];
let mut attrs: Map<Attribute, ValueSet> = Map::new();
attrs.insert(Attribute::ClassName, name_v);
attrs.insert(Attribute::Description, desc_v);
attrs.insert(Attribute::SyncAllowed, sync_allowed_v);
attrs.insert(Attribute::Uuid, uuid_v);
attrs.insert(
Attribute::Class,
vs_iutf8![
EntryClass::Object.into(),
EntryClass::System.into(),
EntryClass::ClassType.into()
],
);
let vs_systemmay = ValueSetIutf8::from_iter(s.systemmay.iter().map(|sm| sm.as_str()));
if let Some(vs) = vs_systemmay {
attrs.insert(Attribute::SystemMay, vs);
}
let vs_systemmust = ValueSetIutf8::from_iter(s.systemmust.iter().map(|sm| sm.as_str()));
if let Some(vs) = vs_systemmust {
attrs.insert(Attribute::SystemMust, vs);
}
Entry {
valid: EntryInit,
state: EntryNew,
attrs,
}
}
}
#[cfg(test)]
mod tests {
use crate::prelude::*;

View file

@ -527,8 +527,7 @@ impl Filter<FilterValid> {
// cases! The exception is *large* filters, especially from the memberof plugin. We
// want to skip these because they can really jam up the server.
// Don't cache anything unless we have valid indexing metadata.
let cacheable = idxmeta.is_some() && FilterResolved::resolve_cacheable(&self.state.inner);
let cacheable = FilterResolved::resolve_cacheable(&self.state.inner);
let cache_key = if cacheable {
// do we have a cache?
@ -537,7 +536,6 @@ impl Filter<FilterValid> {
let cache_key = (ev.get_event_origin_id(), Arc::new(self.clone()));
if let Some(f) = rcache.get(&cache_key) {
// Got it? Shortcut and return!
trace!("shortcut: a resolved filter already exists.");
return Ok(f.as_ref().clone());
};
// Not in cache? Set the cache_key.
@ -576,7 +574,6 @@ impl Filter<FilterValid> {
// if cacheable == false.
if let Some(cache_key) = cache_key {
if let Some(rcache) = rsv_cache.as_mut() {
trace!(?resolved_filt, "inserting filter to resolved cache");
rcache.insert(cache_key, Arc::new(resolved_filt.clone()));
}
}

View file

@ -599,19 +599,19 @@ impl IdmServerProxyWriteTransaction<'_> {
}
let eperm_search_primary_cred = match &eperm.search {
Access::Deny => false,
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential),
};
let eperm_mod_primary_cred = match &eperm.modify_pres {
Access::Deny => false,
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential),
};
let eperm_rem_primary_cred = match &eperm.modify_rem {
Access::Deny => false,
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::PrimaryCredential),
};
@ -620,19 +620,19 @@ impl IdmServerProxyWriteTransaction<'_> {
eperm_search_primary_cred && eperm_mod_primary_cred && eperm_rem_primary_cred;
let eperm_search_passkeys = match &eperm.search {
Access::Deny => false,
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys),
};
let eperm_mod_passkeys = match &eperm.modify_pres {
Access::Deny => false,
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys),
};
let eperm_rem_passkeys = match &eperm.modify_rem {
Access::Deny => false,
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::PassKeys),
};
@ -640,19 +640,19 @@ impl IdmServerProxyWriteTransaction<'_> {
let passkeys_can_edit = eperm_search_passkeys && eperm_mod_passkeys && eperm_rem_passkeys;
let eperm_search_attested_passkeys = match &eperm.search {
Access::Deny => false,
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys),
};
let eperm_mod_attested_passkeys = match &eperm.modify_pres {
Access::Deny => false,
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys),
};
let eperm_rem_attested_passkeys = match &eperm.modify_rem {
Access::Deny => false,
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::AttestedPasskeys),
};
@ -662,19 +662,19 @@ impl IdmServerProxyWriteTransaction<'_> {
&& eperm_rem_attested_passkeys;
let eperm_search_unixcred = match &eperm.search {
Access::Deny => false,
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword),
};
let eperm_mod_unixcred = match &eperm.modify_pres {
Access::Deny => false,
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword),
};
let eperm_rem_unixcred = match &eperm.modify_rem {
Access::Deny => false,
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::UnixPassword),
};
@ -685,19 +685,19 @@ impl IdmServerProxyWriteTransaction<'_> {
&& eperm_rem_unixcred;
let eperm_search_sshpubkey = match &eperm.search {
Access::Deny => false,
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey),
};
let eperm_mod_sshpubkey = match &eperm.modify_pres {
Access::Deny => false,
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey),
};
let eperm_rem_sshpubkey = match &eperm.modify_rem {
Access::Deny => false,
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::SshPublicKey),
};
@ -726,7 +726,7 @@ impl IdmServerProxyWriteTransaction<'_> {
})?;
match &eperm.search {
Access::Deny => false,
Access::Denied => false,
Access::Grant => true,
Access::Allow(attrs) => attrs.contains(&Attribute::SyncCredentialPortal),
}

View file

@ -124,8 +124,8 @@ pub mod prelude {
pub use kanidmd_lib_macros::*;
pub(crate) use crate::valueset::{
ValueSet, ValueSetBool, ValueSetCid, ValueSetIutf8, ValueSetRefer, ValueSetSyntax,
ValueSetT, ValueSetUtf8, ValueSetUuid,
ValueSet, ValueSetBool, ValueSetCid, ValueSetIndex, ValueSetIutf8, ValueSetRefer,
ValueSetSyntax, ValueSetT, ValueSetUtf8, ValueSetUuid,
};
pub(crate) use kanidm_proto::scim_v1::{

View file

@ -620,6 +620,22 @@ macro_rules! vs_syntax {
});
}
#[allow(unused_macros)]
#[macro_export]
macro_rules! vs_index {
() => (
compile_error!("ValueSetIndex needs at least 1 element")
);
($e:expr) => ({
ValueSetIndex::new($e)
});
($e:expr, $($item:expr),*) => ({
let mut x = ValueSetIndex::new($e);
$(assert!(x.push($item));)*
x
});
}
#[allow(unused_macros)]
#[macro_export]
macro_rules! vs_cid {

View file

@ -72,8 +72,6 @@ pub struct BuiltinAcp {
modify_present_attrs: Vec<Attribute>,
modify_removed_attrs: Vec<Attribute>,
modify_classes: Vec<EntryClass>,
modify_present_classes: Vec<EntryClass>,
modify_remove_classes: Vec<EntryClass>,
create_classes: Vec<EntryClass>,
create_attrs: Vec<Attribute>,
}
@ -161,19 +159,9 @@ impl From<BuiltinAcp> for EntryInitNew {
value.modify_removed_attrs.into_iter().for_each(|attr| {
entry.add_ava(Attribute::AcpModifyRemovedAttr, Value::from(attr));
});
value.modify_classes.into_iter().for_each(|class| {
entry.add_ava(Attribute::AcpModifyClass, Value::from(class));
});
value.modify_present_classes.into_iter().for_each(|class| {
entry.add_ava(Attribute::AcpModifyPresentClass, Value::from(class));
});
value.modify_remove_classes.into_iter().for_each(|class| {
entry.add_ava(Attribute::AcpModifyRemoveClass, Value::from(class));
});
value.create_classes.into_iter().for_each(|class| {
entry.add_ava(Attribute::AcpCreateClass, Value::from(class));
});
@ -226,7 +214,7 @@ lazy_static! {
ATTR_RECYCLED.to_string()
)),
modify_removed_attrs: vec![Attribute::Class],
modify_remove_classes: vec![EntryClass::Recycled],
modify_classes: vec![EntryClass::Recycled],
..Default::default()
};
}
@ -437,7 +425,6 @@ lazy_static! {
EntryClass::AccessControlCreate,
EntryClass::AccessControlDelete,
],
..Default::default()
};
}

View file

@ -69,6 +69,7 @@ pub fn phase_1_schema_attrs() -> Vec<EntryInitNew> {
SCHEMA_ATTR_SYNC_TOKEN_SESSION.clone().into(),
SCHEMA_ATTR_UNIX_PASSWORD.clone().into(),
SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION.clone().into(),
SCHEMA_ATTR_DENIED_NAME.clone().into(),
SCHEMA_ATTR_CREDENTIAL_TYPE_MINIMUM.clone().into(),
SCHEMA_ATTR_WEBAUTHN_ATTESTATION_CA_LIST.clone().into(),
// DL4

View file

@ -2,25 +2,52 @@
use crate::constants::entries::{Attribute, EntryClass};
use crate::constants::uuids::*;
use crate::schema::{SchemaAttribute, SchemaClass};
use crate::value::IndexType;
use crate::value::SyntaxType;
lazy_static!(
pub static ref SCHEMA_ATTR_DISPLAYNAME: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DISPLAYNAME,
name: Attribute::DisplayName,
description: "The publicly visible display name of this person".to_string(),
index: vec![IndexType::Equality],
sync_allowed: true,
syntax: SyntaxType::Utf8String,
..Default::default()
};
pub static ref SCHEMA_ATTR_DISPLAYNAME_DL7: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DISPLAYNAME,
name: Attribute::DisplayName,
description: "The publicly visible display name of this person".to_string(),
indexed: true,
index: vec![IndexType::Equality, IndexType::SubString],
sync_allowed: true,
syntax: SyntaxType::Utf8String,
..Default::default()
};
pub static ref SCHEMA_ATTR_MAIL: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_MAIL,
name: Attribute::Mail,
description: "Mail addresses of the object".to_string(),
index: vec![IndexType::Equality],
unique: true,
multivalue: true,
sync_allowed: true,
syntax: SyntaxType::EmailAddress,
..Default::default()
};
pub static ref SCHEMA_ATTR_MAIL_DL7: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_MAIL,
name: Attribute::Mail,
description: "Mail addresses of the object".to_string(),
indexed: true,
index: vec![IndexType::Equality, IndexType::SubString],
unique: true,
multivalue: true,
sync_allowed: true,
@ -32,7 +59,8 @@ pub static ref SCHEMA_ATTR_EC_KEY_PRIVATE: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_EC_KEY_PRIVATE,
name: Attribute::IdVerificationEcKey,
description: "Account verification private key".to_string(),
indexed: true,
index: vec![IndexType::Presence],
unique: false,
sync_allowed: false,
syntax: SyntaxType::EcKeyPrivate,
@ -54,17 +82,30 @@ pub static ref SCHEMA_ATTR_PRIMARY_CREDENTIAL: SchemaAttribute = SchemaAttribute
uuid: UUID_SCHEMA_ATTR_PRIMARY_CREDENTIAL,
name: Attribute::PrimaryCredential,
description: "Primary credential material of the account for authentication interactively".to_string(),
indexed: true,
index: vec![IndexType::Presence],
sync_allowed: true,
syntax: SyntaxType::Credential,
..Default::default()
};
pub static ref SCHEMA_ATTR_LEGALNAME: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_LEGALNAME,
name: Attribute::LegalName,
description: "The private and sensitive legal name of this person".to_string(),
index: vec![IndexType::Equality],
sync_allowed: true,
syntax: SyntaxType::Utf8String,
..Default::default()
};
pub static ref SCHEMA_ATTR_LEGALNAME_DL7: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_LEGALNAME,
name: Attribute::LegalName,
description: "The private and sensitive legal name of this person".to_string(),
indexed: true,
index: vec![IndexType::Equality, IndexType::SubString],
sync_allowed: true,
syntax: SyntaxType::Utf8String,
..Default::default()
@ -74,7 +115,8 @@ pub static ref SCHEMA_ATTR_NAME_HISTORY: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_NAME_HISTORY,
name: Attribute::NameHistory,
description: "The history of names that a person has had".to_string(),
indexed: true,
index: vec![IndexType::Equality],
multivalue: true,
sync_allowed: true,
syntax: SyntaxType::AuditLogString,
@ -85,6 +127,7 @@ pub static ref SCHEMA_ATTR_RADIUS_SECRET: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_RADIUS_SECRET,
name: Attribute::RadiusSecret,
description: "The accounts generated radius secret for device network authentication".to_string(),
sync_allowed: true,
syntax: SyntaxType::SecretUtf8String,
..Default::default()
@ -94,7 +137,8 @@ pub static ref SCHEMA_ATTR_DOMAIN_NAME: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DOMAIN_NAME,
name: Attribute::DomainName,
description: "The domain's DNS name for webauthn and SPN generation purposes".to_string(),
indexed: true,
index: vec![IndexType::Equality, IndexType::Presence],
unique: true,
syntax: SyntaxType::Utf8StringIname,
..Default::default()
@ -104,6 +148,7 @@ pub static ref SCHEMA_ATTR_LDAP_ALLOW_UNIX_PW_BIND: SchemaAttribute = SchemaAttr
uuid: UUID_SCHEMA_ATTR_LDAP_ALLOW_UNIX_PW_BIND,
name: Attribute::LdapAllowUnixPwBind,
description: "Configuration to enable binds to LDAP objects using their UNIX password".to_string(),
unique: false,
syntax: SyntaxType::Boolean,
..Default::default()
@ -113,6 +158,7 @@ pub static ref SCHEMA_ATTR_DOMAIN_LDAP_BASEDN: SchemaAttribute = SchemaAttribute
uuid: UUID_SCHEMA_ATTR_DOMAIN_LDAP_BASEDN,
name: Attribute::DomainLdapBasedn,
description: "The domain's optional ldap basedn. If unset defaults to domain components of domain name".to_string(),
unique: true,
syntax: SyntaxType::Utf8StringInsensitive,
..Default::default()
@ -122,6 +168,7 @@ pub static ref SCHEMA_ATTR_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES: SchemaAttribute =
uuid: UUID_SCHEMA_ATTR_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES,
name: Attribute::LdapMaxQueryableAttrs,
description: "The maximum number of LDAP attributes that can be queried in one operation".to_string(),
multivalue: false,
sync_allowed: true,
syntax: SyntaxType::Uint32,
@ -132,7 +179,8 @@ pub static ref SCHEMA_ATTR_DOMAIN_DISPLAY_NAME: SchemaAttribute = SchemaAttribut
uuid: UUID_SCHEMA_ATTR_DOMAIN_DISPLAY_NAME,
name: Attribute::DomainDisplayName,
description: "The user-facing display name of the Kanidm domain".to_string(),
indexed: true,
index: vec![IndexType::Equality],
syntax: SyntaxType::Utf8String,
..Default::default()
};
@ -141,7 +189,8 @@ pub static ref SCHEMA_ATTR_DOMAIN_UUID: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DOMAIN_UUID,
name: Attribute::DomainUuid,
description: "The domain's uuid, used in CSN and trust relationships".to_string(),
indexed: true,
index: vec![IndexType::Equality],
unique: true,
syntax: SyntaxType::Uuid,
..Default::default()
@ -151,16 +200,27 @@ pub static ref SCHEMA_ATTR_DOMAIN_SSID: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DOMAIN_SSID,
name: Attribute::DomainSsid,
description: "The domains site-wide SSID for device autoconfiguration of wireless".to_string(),
indexed: true,
index: vec![IndexType::Equality],
unique: true,
syntax: SyntaxType::Utf8String,
..Default::default()
};
pub static ref SCHEMA_ATTR_DENIED_NAME: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DENIED_NAME,
name: Attribute::DeniedName,
description: "Iname values that are not allowed to be used in 'name'.".to_string(),
syntax: SyntaxType::Utf8StringIname,
..Default::default()
};
pub static ref SCHEMA_ATTR_DENIED_NAME_DL10: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DENIED_NAME,
name: Attribute::DeniedName,
description: "Iname values that are not allowed to be used in 'name'.".to_string(),
syntax: SyntaxType::Utf8StringIname,
multivalue: true,
..Default::default()
@ -170,6 +230,7 @@ pub static ref SCHEMA_ATTR_DOMAIN_TOKEN_KEY: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DOMAIN_TOKEN_KEY,
name: Attribute::DomainTokenKey,
description: "The domain token encryption private key (NOT USED)".to_string(),
syntax: SyntaxType::SecretUtf8String,
..Default::default()
};
@ -187,7 +248,8 @@ pub static ref SCHEMA_ATTR_GIDNUMBER: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_GIDNUMBER,
name: Attribute::GidNumber,
description: "The groupid (uid) number of a group or account.to_string(). This is the same value as the UID number on posix accounts for security reasons".to_string(),
indexed: true,
index: vec![IndexType::Equality],
unique: true,
sync_allowed: true,
syntax: SyntaxType::Uint32,
@ -198,6 +260,7 @@ pub static ref SCHEMA_ATTR_BADLIST_PASSWORD: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_BADLIST_PASSWORD,
name: Attribute::BadlistPassword,
description: "A password that is badlisted meaning that it can not be set as a valid password by any user account".to_string(),
multivalue: true,
syntax: SyntaxType::Utf8StringInsensitive,
..Default::default()
@ -207,6 +270,7 @@ pub static ref SCHEMA_ATTR_AUTH_SESSION_EXPIRY: SchemaAttribute = SchemaAttribut
uuid: UUID_SCHEMA_ATTR_AUTH_SESSION_EXPIRY,
name: Attribute::AuthSessionExpiry,
description: "An expiration time for an authentication session".to_string(),
syntax: SyntaxType::Uint32,
..Default::default()
};
@ -215,6 +279,7 @@ pub static ref SCHEMA_ATTR_AUTH_PRIVILEGE_EXPIRY: SchemaAttribute = SchemaAttrib
uuid: UUID_SCHEMA_ATTR_AUTH_PRIVILEGE_EXPIRY,
name: Attribute::PrivilegeExpiry,
description: "An expiration time for a privileged authentication session".to_string(),
syntax: SyntaxType::Uint32,
..Default::default()
};
@ -223,6 +288,7 @@ pub static ref SCHEMA_ATTR_AUTH_PASSWORD_MINIMUM_LENGTH: SchemaAttribute = Schem
uuid: UUID_SCHEMA_ATTR_AUTH_PASSWORD_MINIMUM_LENGTH,
name: Attribute::AuthPasswordMinimumLength,
description: "Minimum length of passwords".to_string(),
syntax: SyntaxType::Uint32,
..Default::default()
};
@ -231,6 +297,7 @@ pub static ref SCHEMA_ATTR_LOGINSHELL: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_LOGINSHELL,
name: Attribute::LoginShell,
description: "A POSIX user's UNIX login shell".to_string(),
sync_allowed: true,
syntax: SyntaxType::Utf8StringInsensitive,
..Default::default()
@ -240,7 +307,8 @@ pub static ref SCHEMA_ATTR_UNIX_PASSWORD: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_UNIX_PASSWORD,
name: Attribute::UnixPassword,
description: "A POSIX user's UNIX login password".to_string(),
indexed: true,
index: vec![IndexType::Presence],
syntax: SyntaxType::Credential,
..Default::default()
};
@ -249,7 +317,8 @@ pub static ref SCHEMA_ATTR_NSUNIQUEID: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_NSUNIQUEID,
name: Attribute::NsUniqueId,
description: "A unique id compatibility for 389-ds/dsee".to_string(),
indexed: true,
index: vec![IndexType::Equality],
unique: true,
sync_allowed: true,
syntax: SyntaxType::NsUniqueId,
@ -260,6 +329,7 @@ pub static ref SCHEMA_ATTR_ACCOUNT_EXPIRE: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_ACCOUNT_EXPIRE,
name: Attribute::AccountExpire,
description: "The datetime after which this account no longer may authenticate".to_string(),
sync_allowed: true,
syntax: SyntaxType::DateTime,
..Default::default()
@ -269,6 +339,7 @@ pub static ref SCHEMA_ATTR_ACCOUNT_VALID_FROM: SchemaAttribute = SchemaAttribute
uuid: UUID_SCHEMA_ATTR_ACCOUNT_VALID_FROM,
name: Attribute::AccountValidFrom,
description: "The datetime after which this account may commence authenticating".to_string(),
sync_allowed: true,
syntax: SyntaxType::DateTime,
..Default::default()
@ -278,6 +349,7 @@ pub static ref SCHEMA_ATTR_WEBAUTHN_ATTESTATION_CA_LIST: SchemaAttribute = Schem
uuid: UUID_SCHEMA_ATTR_WEBAUTHN_ATTESTATION_CA_LIST,
name: Attribute::WebauthnAttestationCaList,
description: "A set of CA's that limit devices that can be used with webauthn".to_string(),
syntax: SyntaxType::WebauthnAttestationCaList,
multivalue: true,
..Default::default()
@ -287,16 +359,27 @@ pub static ref SCHEMA_ATTR_OAUTH2_RS_NAME: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_NAME,
name: Attribute::OAuth2RsName,
description: "The unique name of an external Oauth2 resource".to_string(),
indexed: true,
index: vec![IndexType::Equality],
unique: true,
syntax: SyntaxType::Utf8StringIname,
..Default::default()
};
pub static ref SCHEMA_ATTR_OAUTH2_RS_ORIGIN: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_ORIGIN,
name: Attribute::OAuth2RsOrigin,
description: "The origin domain of an oauth2 resource server".to_string(),
syntax: SyntaxType::Url,
..Default::default()
};
pub static ref SCHEMA_ATTR_OAUTH2_RS_ORIGIN_DL7: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_ORIGIN,
name: Attribute::OAuth2RsOrigin,
description: "The origin domain of an OAuth2 client".to_string(),
syntax: SyntaxType::Url,
multivalue: true,
..Default::default()
@ -306,6 +389,7 @@ pub static ref SCHEMA_ATTR_OAUTH2_RS_ORIGIN_LANDING: SchemaAttribute = SchemaAtt
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_ORIGIN_LANDING,
name: Attribute::OAuth2RsOriginLanding,
description: "The landing page of an RS, that will automatically trigger the auth process".to_string(),
syntax: SyntaxType::Url,
..Default::default()
};
@ -315,6 +399,7 @@ pub static ref SCHEMA_ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT_DL4: SchemaAttribute
uuid: UUID_SCHEMA_ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT,
name: Attribute::OAuth2AllowLocalhostRedirect,
description: "Allow public clients associated to this RS to redirect to localhost".to_string(),
syntax: SyntaxType::Boolean,
..Default::default()
};
@ -323,7 +408,8 @@ pub static ref SCHEMA_ATTR_OAUTH2_RS_CLAIM_MAP_DL4: SchemaAttribute = SchemaAttr
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_CLAIM_MAP,
name: Attribute::OAuth2RsClaimMap,
description: "A set of custom claims mapped to group memberships of accounts".to_string(),
indexed: true,
index: vec![IndexType::Equality],
multivalue: true,
// CHANGE ME
syntax: SyntaxType::OauthClaimMap,
@ -334,7 +420,8 @@ pub static ref SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP: SchemaAttribute = SchemaAttribut
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP,
name: Attribute::OAuth2RsScopeMap,
description: "A reference to a group mapped to scopes for the associated oauth2 resource server".to_string(),
indexed: true,
index: vec![IndexType::Equality],
multivalue: true,
syntax: SyntaxType::OauthScopeMap,
..Default::default()
@ -344,7 +431,8 @@ pub static ref SCHEMA_ATTR_OAUTH2_RS_SUP_SCOPE_MAP: SchemaAttribute = SchemaAttr
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_SUP_SCOPE_MAP,
name: Attribute::OAuth2RsSupScopeMap,
description: "A reference to a group mapped to scopes for the associated oauth2 resource server".to_string(),
indexed: true,
index: vec![IndexType::Equality],
multivalue: true,
syntax: SyntaxType::OauthScopeMap,
..Default::default()
@ -354,6 +442,7 @@ pub static ref SCHEMA_ATTR_OAUTH2_RS_BASIC_SECRET: SchemaAttribute = SchemaAttri
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_BASIC_SECRET,
name: Attribute::OAuth2RsBasicSecret,
description: "When using oauth2 basic authentication, the secret string of the resource server".to_string(),
syntax: SyntaxType::SecretUtf8String,
..Default::default()
};
@ -362,6 +451,7 @@ pub static ref SCHEMA_ATTR_OAUTH2_RS_TOKEN_KEY: SchemaAttribute = SchemaAttribut
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_TOKEN_KEY,
name: Attribute::OAuth2RsTokenKey,
description: "An oauth2 resource servers unique token signing key".to_string(),
syntax: SyntaxType::SecretUtf8String,
..Default::default()
};
@ -370,6 +460,7 @@ pub static ref SCHEMA_ATTR_OAUTH2_RS_IMPLICIT_SCOPES: SchemaAttribute = SchemaAt
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_IMPLICIT_SCOPES,
name: Attribute::OAuth2RsImplicitScopes,
description: "An oauth2 resource servers scopes that are implicitly granted to all users".to_string(),
multivalue: true,
syntax: SyntaxType::OauthScope,
..Default::default()
@ -379,7 +470,8 @@ pub static ref SCHEMA_ATTR_OAUTH2_CONSENT_SCOPE_MAP: SchemaAttribute = SchemaAtt
uuid: UUID_SCHEMA_ATTR_OAUTH2_CONSENT_SCOPE_MAP,
name: Attribute::OAuth2ConsentScopeMap,
description: "A set of scopes mapped from a relying server to a user, where the user has previously consented to the following. If changed or deleted, consent will be re-sought".to_string(),
indexed: true,
index: vec![IndexType::Equality],
multivalue: true,
syntax: SyntaxType::OauthScopeMap,
..Default::default()
@ -389,6 +481,7 @@ pub static ref SCHEMA_ATTR_OAUTH2_STRICT_REDIRECT_URI_DL7: SchemaAttribute = Sch
uuid: UUID_SCHEMA_ATTR_OAUTH2_STRICT_REDIRECT_URI,
name: Attribute::OAuth2StrictRedirectUri,
description: "Represents if strict redirect uri enforcement is enabled.".to_string(),
syntax: SyntaxType::Boolean,
..Default::default()
};
@ -398,6 +491,7 @@ pub static ref SCHEMA_ATTR_OAUTH2_DEVICE_FLOW_ENABLE_DL9: SchemaAttribute = Sche
uuid: UUID_SCHEMA_ATTR_OAUTH2_DEVICE_FLOW_ENABLE,
name: Attribute::OAuth2DeviceFlowEnable,
description: "Represents if OAuth2 Device Flow is permitted on this client.".to_string(),
syntax: SyntaxType::Boolean,
..Default::default()
};
@ -406,6 +500,7 @@ pub static ref SCHEMA_ATTR_ES256_PRIVATE_KEY_DER: SchemaAttribute = SchemaAttrib
uuid: UUID_SCHEMA_ATTR_ES256_PRIVATE_KEY_DER,
name: Attribute::Es256PrivateKeyDer,
description: "An es256 private key".to_string(),
syntax: SyntaxType::PrivateBinary,
..Default::default()
};
@ -414,6 +509,7 @@ pub static ref SCHEMA_ATTR_RS256_PRIVATE_KEY_DER: SchemaAttribute = SchemaAttrib
uuid: UUID_SCHEMA_ATTR_RS256_PRIVATE_KEY_DER,
name: Attribute::Rs256PrivateKeyDer,
description: "An rs256 private key".to_string(),
syntax: SyntaxType::PrivateBinary,
..Default::default()
};
@ -422,7 +518,8 @@ pub static ref SCHEMA_ATTR_JWS_ES256_PRIVATE_KEY: SchemaAttribute = SchemaAttrib
uuid: UUID_SCHEMA_ATTR_JWS_ES256_PRIVATE_KEY,
name: Attribute::JwsEs256PrivateKey,
description: "An es256 private key for jws".to_string(),
indexed: true,
index: vec![IndexType::Equality],
unique: true,
syntax: SyntaxType::JwsKeyEs256,
..Default::default()
@ -433,6 +530,7 @@ pub static ref SCHEMA_ATTR_PRIVATE_COOKIE_KEY: SchemaAttribute = SchemaAttribute
uuid: UUID_SCHEMA_ATTR_PRIVATE_COOKIE_KEY,
name: Attribute::PrivateCookieKey,
description: "An private cookie hmac key".to_string(),
syntax: SyntaxType::PrivateBinary,
..Default::default()
};
@ -441,6 +539,7 @@ pub static ref SCHEMA_ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE: SchemaAttr
uuid: UUID_SCHEMA_ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE,
name: Attribute::OAuth2AllowInsecureClientDisablePkce,
description: "Allows disabling of PKCE for insecure OAuth2 clients".to_string(),
syntax: SyntaxType::Boolean,
..Default::default()
};
@ -449,6 +548,7 @@ pub static ref SCHEMA_ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE: SchemaAttribute = Sc
uuid: UUID_SCHEMA_ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE,
name: Attribute::OAuth2JwtLegacyCryptoEnable,
description: "Allows enabling legacy JWT cryptograhpy for clients".to_string(),
syntax: SyntaxType::Boolean,
..Default::default()
};
@ -457,7 +557,8 @@ pub static ref SCHEMA_ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN: SchemaAttribute = Sch
uuid: UUID_SCHEMA_ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN,
name: Attribute::CredentialUpdateIntentToken,
description: "The status of a credential update intent token".to_string(),
indexed: true,
index: vec![IndexType::Equality],
multivalue: true,
syntax: SyntaxType::IntentToken,
..Default::default()
@ -467,7 +568,8 @@ pub static ref SCHEMA_ATTR_PASSKEYS: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_PASSKEYS,
name: Attribute::PassKeys,
description: "A set of registered passkeys".to_string(),
indexed: true,
index: vec![IndexType::Equality],
multivalue: true,
sync_allowed: true,
syntax: SyntaxType::Passkey,
@ -478,7 +580,8 @@ pub static ref SCHEMA_ATTR_ATTESTED_PASSKEYS: SchemaAttribute = SchemaAttribute
uuid: UUID_SCHEMA_ATTR_ATTESTED_PASSKEYS,
name: Attribute::AttestedPasskeys,
description: "A set of registered device keys".to_string(),
indexed: true,
index: vec![IndexType::Equality],
multivalue: true,
sync_allowed: true,
syntax: SyntaxType::AttestedPasskey,
@ -489,6 +592,7 @@ pub static ref SCHEMA_ATTR_DYNGROUP_FILTER: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DYNGROUP_FILTER,
name: Attribute::DynGroupFilter,
description: "A filter describing the set of entries to add to a dynamic group".to_string(),
syntax: SyntaxType::JsonFilter,
..Default::default()
};
@ -497,6 +601,7 @@ pub static ref SCHEMA_ATTR_OAUTH2_PREFER_SHORT_USERNAME: SchemaAttribute = Schem
uuid: UUID_SCHEMA_ATTR_OAUTH2_PREFER_SHORT_USERNAME,
name: Attribute::OAuth2PreferShortUsername,
description: "Use 'name' instead of 'spn' in the preferred_username claim".to_string(),
syntax: SyntaxType::Boolean,
..Default::default()
};
@ -505,7 +610,8 @@ pub static ref SCHEMA_ATTR_API_TOKEN_SESSION: SchemaAttribute = SchemaAttribute
uuid: UUID_SCHEMA_ATTR_API_TOKEN_SESSION,
name: Attribute::ApiTokenSession,
description: "A session entry related to an issued API token".to_string(),
indexed: true,
index: vec![IndexType::Equality],
unique: true,
multivalue: true,
syntax: SyntaxType::ApiToken,
@ -516,7 +622,8 @@ pub static ref SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION: SchemaAttribute = SchemaAttr
uuid: UUID_SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION,
name: Attribute::UserAuthTokenSession,
description: "A session entry related to an issued user auth token".to_string(),
indexed: true,
index: vec![IndexType::Equality],
unique: true,
multivalue: true,
syntax: SyntaxType::Session,
@ -527,7 +634,8 @@ pub static ref SCHEMA_ATTR_OAUTH2_SESSION: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_OAUTH2_SESSION,
name: Attribute::OAuth2Session,
description: "A session entry to an active oauth2 session, bound to a parent user auth token".to_string(),
indexed: true,
index: vec![IndexType::Equality],
multivalue: true,
syntax: SyntaxType::Oauth2Session,
..Default::default()
@ -537,7 +645,8 @@ pub static ref SCHEMA_ATTR_SYNC_TOKEN_SESSION: SchemaAttribute = SchemaAttribute
uuid: UUID_SCHEMA_ATTR_SYNC_TOKEN_SESSION,
name: Attribute::SyncTokenSession,
description: "A session entry related to an issued sync token".to_string(),
indexed: true,
index: vec![IndexType::Equality],
unique: true,
syntax: SyntaxType::ApiToken,
..Default::default()
@ -547,6 +656,7 @@ pub static ref SCHEMA_ATTR_SYNC_COOKIE: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_SYNC_COOKIE,
name: Attribute::SyncCookie,
description: "A private sync cookie for a remote IDM source".to_string(),
syntax: SyntaxType::PrivateBinary,
..Default::default()
};
@ -555,7 +665,8 @@ pub static ref SCHEMA_ATTR_GRANT_UI_HINT: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_GRANT_UI_HINT,
name: Attribute::GrantUiHint,
description: "A UI hint that is granted via membership to a group".to_string(),
indexed: true,
index: vec![IndexType::Equality],
multivalue: true,
syntax: SyntaxType::UiHint,
..Default::default()
@ -565,6 +676,7 @@ pub static ref SCHEMA_ATTR_SYNC_CREDENTIAL_PORTAL: SchemaAttribute = SchemaAttri
uuid: UUID_SCHEMA_ATTR_SYNC_CREDENTIAL_PORTAL,
name: Attribute::SyncCredentialPortal,
description: "The url of an external credential portal for synced accounts to visit to update their credentials".to_string(),
syntax: SyntaxType::Url,
..Default::default()
};
@ -573,6 +685,7 @@ pub static ref SCHEMA_ATTR_SYNC_YIELD_AUTHORITY: SchemaAttribute = SchemaAttribu
uuid: UUID_SCHEMA_ATTR_SYNC_YIELD_AUTHORITY,
name: Attribute::SyncYieldAuthority,
description: "A set of attributes that have their authority yielded to Kanidm in a sync agreement".to_string(),
multivalue: true,
syntax: SyntaxType::Utf8StringInsensitive,
..Default::default()
@ -582,6 +695,7 @@ pub static ref SCHEMA_ATTR_CREDENTIAL_TYPE_MINIMUM: SchemaAttribute = SchemaAttr
uuid: UUID_SCHEMA_ATTR_CREDENTIAL_TYPE_MINIMUM,
name: Attribute::CredentialTypeMinimum,
description: "The minimum level of credential type that can satisfy this policy".to_string(),
multivalue: false,
syntax: SyntaxType::CredentialType,
..Default::default()
@ -591,6 +705,7 @@ pub static ref SCHEMA_ATTR_LIMIT_SEARCH_MAX_RESULTS_DL6: SchemaAttribute = Schem
uuid: UUID_SCHEMA_ATTR_LIMIT_SEARCH_MAX_RESULTS,
name: Attribute::LimitSearchMaxResults,
description: "The maximum number of query results that may be returned in a single operation".to_string(),
multivalue: false,
syntax: SyntaxType::Uint32,
..Default::default()
@ -600,6 +715,7 @@ pub static ref SCHEMA_ATTR_LIMIT_SEARCH_MAX_FILTER_TEST_DL6: SchemaAttribute = S
uuid: UUID_SCHEMA_ATTR_LIMIT_SEARCH_MAX_FILTER_TEST,
name: Attribute::LimitSearchMaxFilterTest,
description: "The maximum number of entries that may be examined in a partially indexed query".to_string(),
multivalue: false,
syntax: SyntaxType::Uint32,
..Default::default()
@ -619,7 +735,6 @@ pub static ref SCHEMA_ATTR_KEY_PROVIDER_DL6: SchemaAttribute = SchemaAttribute {
name: Attribute::KeyProvider,
description: "".to_string(),
multivalue: false,
indexed: true,
syntax: SyntaxType::ReferenceUuid,
..Default::default()
};
@ -685,7 +800,6 @@ pub static ref SCHEMA_ATTR_REFERS_DL7: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_REFERS,
name: Attribute::Refers,
description: "A reference to linked object".to_string(),
indexed: true,
multivalue: false,
syntax: SyntaxType::ReferenceUuid,
..Default::default()
@ -695,8 +809,8 @@ pub static ref SCHEMA_ATTR_LINKED_GROUP_DL8: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_LINKED_GROUP,
name: Attribute::LinkedGroup,
description: "A reference linking a group to an entry".to_string(),
multivalue: false,
indexed: true,
syntax: SyntaxType::ReferenceUuid,
..Default::default()
};
@ -705,6 +819,7 @@ pub static ref SCHEMA_ATTR_ALLOW_PRIMARY_CRED_FALLBACK_DL8: SchemaAttribute = Sc
uuid: UUID_SCHEMA_ATTR_ALLOW_PRIMARY_CRED_FALLBACK,
name: Attribute::AllowPrimaryCredFallback,
description: "Allow fallback to primary password if no POSIX password exists".to_string(),
multivalue: false,
syntax: SyntaxType::Boolean,
..Default::default()
@ -723,13 +838,57 @@ pub static ref SCHEMA_ATTR_APPLICATION_PASSWORD_DL8: SchemaAttribute = SchemaAtt
uuid: UUID_SCHEMA_ATTR_APPLICATION_PASSWORD,
name: Attribute::ApplicationPassword,
description: "A set of application passwords".to_string(),
multivalue: true,
indexed: true,
syntax: SyntaxType::ApplicationPassword,
..Default::default()
};
// === classes ===
pub static ref SCHEMA_CLASS_PERSON: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_PERSON,
name: EntryClass::Person.into(),
description: "Object representation of a person".to_string(),
sync_allowed: true,
systemmay: vec![
Attribute::Mail,
Attribute::LegalName,
],
systemmust: vec![
Attribute::DisplayName,
Attribute::Name,
Attribute::IdVerificationEcKey],
..Default::default()
};
pub static ref SCHEMA_CLASS_PERSON_DL5: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_PERSON,
name: EntryClass::Person.into(),
description: "Object representation of a person".to_string(),
sync_allowed: true,
systemmay: vec![
Attribute::PrimaryCredential,
Attribute::PassKeys,
Attribute::AttestedPasskeys,
Attribute::CredentialUpdateIntentToken,
Attribute::SshPublicKey,
Attribute::RadiusSecret,
Attribute::OAuth2ConsentScopeMap,
Attribute::UserAuthTokenSession,
Attribute::OAuth2Session,
Attribute::Mail,
Attribute::LegalName,
],
systemmust: vec![
Attribute::IdVerificationEcKey
],
systemexcludes: vec![EntryClass::ServiceAccount.into(), EntryClass::Application.into()],
..Default::default()
};
pub static ref SCHEMA_CLASS_PERSON_DL8: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_PERSON,
name: EntryClass::Person.into(),
@ -802,6 +961,24 @@ pub static ref SCHEMA_CLASS_DYNGROUP: SchemaClass = SchemaClass {
..Default::default()
};
pub static ref SCHEMA_CLASS_ACCOUNT_POLICY_DL6: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_ACCOUNT_POLICY,
name: EntryClass::AccountPolicy.into(),
description: "Policies applied to accounts that are members of a group".to_string(),
systemmay: vec![
Attribute::AuthSessionExpiry,
Attribute::PrivilegeExpiry,
Attribute::AuthPasswordMinimumLength,
Attribute::CredentialTypeMinimum,
Attribute::WebauthnAttestationCaList,
Attribute::LimitSearchMaxResults,
Attribute::LimitSearchMaxFilterTest,
],
systemsupplements: vec![Attribute::Group.into()],
..Default::default()
};
pub static ref SCHEMA_CLASS_ACCOUNT_POLICY_DL8: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_ACCOUNT_POLICY,
name: EntryClass::AccountPolicy.into(),
@ -821,6 +998,40 @@ pub static ref SCHEMA_CLASS_ACCOUNT_POLICY_DL8: SchemaClass = SchemaClass {
..Default::default()
};
pub static ref SCHEMA_CLASS_ACCOUNT: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_ACCOUNT,
name: EntryClass::Account.into(),
description: "Object representation of an account".to_string(),
sync_allowed: true,
systemmay: vec![
Attribute::PrimaryCredential,
Attribute::PassKeys,
Attribute::AttestedPasskeys,
Attribute::CredentialUpdateIntentToken,
Attribute::SshPublicKey,
Attribute::RadiusSecret,
Attribute::AccountExpire,
Attribute::AccountValidFrom,
Attribute::Mail,
Attribute::OAuth2ConsentScopeMap,
Attribute::UserAuthTokenSession,
Attribute::OAuth2Session,
Attribute::Description,
Attribute::NameHistory,
],
systemmust: vec![
Attribute::DisplayName,
Attribute::Name,
Attribute::Spn
],
systemsupplements: vec![
EntryClass::Person.into(),
EntryClass::ServiceAccount.into(),
],
..Default::default()
};
pub static ref SCHEMA_CLASS_ACCOUNT_DL5: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_ACCOUNT,
name: EntryClass::Account.into(),
@ -845,6 +1056,29 @@ pub static ref SCHEMA_CLASS_ACCOUNT_DL5: SchemaClass = SchemaClass {
..Default::default()
};
pub static ref SCHEMA_CLASS_SERVICE_ACCOUNT_DL6: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_SERVICE_ACCOUNT,
name: EntryClass::ServiceAccount.into(),
description: "Object representation of service account".to_string(),
sync_allowed: true,
systemmay: vec![
Attribute::SshPublicKey,
Attribute::UserAuthTokenSession,
Attribute::OAuth2Session,
Attribute::OAuth2ConsentScopeMap,
Attribute::Description,
Attribute::Mail,
Attribute::PrimaryCredential,
Attribute::ApiTokenSession,
Attribute::JwsEs256PrivateKey,
],
systemexcludes: vec![EntryClass::Person.into()],
..Default::default()
};
pub static ref SCHEMA_CLASS_SERVICE_ACCOUNT_DL7: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_SERVICE_ACCOUNT,
name: EntryClass::ServiceAccount.into(),
@ -866,6 +1100,23 @@ pub static ref SCHEMA_CLASS_SERVICE_ACCOUNT_DL7: SchemaClass = SchemaClass {
..Default::default()
};
pub static ref SCHEMA_CLASS_SYNC_ACCOUNT_DL6: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_SYNC_ACCOUNT,
name: EntryClass::SyncAccount.into(),
description: "Object representation of sync account".to_string(),
systemmust: vec![Attribute::Name],
systemmay: vec![
Attribute::SyncTokenSession,
Attribute::SyncCookie,
Attribute::SyncCredentialPortal,
Attribute::SyncYieldAuthority,
Attribute::JwsEs256PrivateKey,
],
systemexcludes: vec![EntryClass::Account.into()],
..Default::default()
};
pub static ref SCHEMA_CLASS_SYNC_ACCOUNT_DL7: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_SYNC_ACCOUNT,
name: EntryClass::SyncAccount.into(),
@ -882,6 +1133,100 @@ pub static ref SCHEMA_CLASS_SYNC_ACCOUNT_DL7: SchemaClass = SchemaClass {
..Default::default()
};
pub static ref SCHEMA_CLASS_DOMAIN_INFO_DL6: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_DOMAIN_INFO,
name: EntryClass::DomainInfo.into(),
description: "Local domain information and configuration".to_string(),
systemmay: vec![
Attribute::DomainSsid,
Attribute::DomainLdapBasedn,
Attribute::LdapAllowUnixPwBind,
Attribute::PrivateCookieKey,
Attribute::FernetPrivateKeyStr,
Attribute::Es256PrivateKeyDer,
Attribute::PatchLevel,
Attribute::DomainDevelopmentTaint,
],
systemmust: vec![
Attribute::Name,
Attribute::DomainUuid,
Attribute::DomainName,
Attribute::DomainDisplayName,
Attribute::Version,
],
..Default::default()
};
pub static ref SCHEMA_CLASS_DOMAIN_INFO_DL7: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_DOMAIN_INFO,
name: EntryClass::DomainInfo.into(),
description: "Local domain information and configuration".to_string(),
systemmay: vec![
Attribute::DomainSsid,
Attribute::DomainLdapBasedn,
Attribute::LdapAllowUnixPwBind,
Attribute::PatchLevel,
Attribute::DomainDevelopmentTaint,
],
systemmust: vec![
Attribute::Name,
Attribute::DomainUuid,
Attribute::DomainName,
Attribute::DomainDisplayName,
Attribute::Version,
],
..Default::default()
};
pub static ref SCHEMA_CLASS_DOMAIN_INFO_DL8: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_DOMAIN_INFO,
name: EntryClass::DomainInfo.into(),
description: "Local domain information and configuration".to_string(),
systemmay: vec![
Attribute::DomainSsid,
Attribute::DomainLdapBasedn,
Attribute::LdapAllowUnixPwBind,
Attribute::Image,
Attribute::PatchLevel,
Attribute::DomainDevelopmentTaint,
],
systemmust: vec![
Attribute::Name,
Attribute::DomainUuid,
Attribute::DomainName,
Attribute::DomainDisplayName,
Attribute::Version,
],
..Default::default()
};
pub static ref SCHEMA_CLASS_DOMAIN_INFO_DL9: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_DOMAIN_INFO,
name: EntryClass::DomainInfo.into(),
description: "Local domain information and configuration".to_string(),
systemmay: vec![
Attribute::DomainSsid,
Attribute::DomainLdapBasedn,
Attribute::LdapAllowUnixPwBind,
Attribute::Image,
Attribute::PatchLevel,
Attribute::DomainDevelopmentTaint,
Attribute::DomainAllowEasterEggs,
],
systemmust: vec![
Attribute::Name,
Attribute::DomainUuid,
Attribute::DomainName,
Attribute::DomainDisplayName,
Attribute::Version,
],
..Default::default()
};
pub static ref SCHEMA_CLASS_DOMAIN_INFO_DL10: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_DOMAIN_INFO,
name: EntryClass::DomainInfo.into(),
@ -945,6 +1290,83 @@ pub static ref SCHEMA_CLASS_SYSTEM_CONFIG: SchemaClass = SchemaClass {
..Default::default()
};
pub static ref SCHEMA_CLASS_OAUTH2_RS_DL4: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS,
name: EntryClass::OAuth2ResourceServer.into(),
description: "The class representing a configured Oauth2 Resource Server".to_string(),
systemmay: vec![
Attribute::Description,
Attribute::OAuth2RsScopeMap,
Attribute::OAuth2RsSupScopeMap,
Attribute::Rs256PrivateKeyDer,
Attribute::OAuth2JwtLegacyCryptoEnable,
Attribute::OAuth2PreferShortUsername,
Attribute::OAuth2RsOriginLanding,
Attribute::Image,
Attribute::OAuth2RsClaimMap,
],
systemmust: vec![
Attribute::OAuth2RsName,
Attribute::DisplayName,
Attribute::OAuth2RsOrigin,
Attribute::OAuth2RsTokenKey,
Attribute::Es256PrivateKeyDer,
],
..Default::default()
};
pub static ref SCHEMA_CLASS_OAUTH2_RS_DL5: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS,
name: EntryClass::OAuth2ResourceServer.into(),
description: "The class representing a configured Oauth2 Resource Server".to_string(),
systemmay: vec![
Attribute::Description,
Attribute::OAuth2RsScopeMap,
Attribute::OAuth2RsSupScopeMap,
Attribute::Rs256PrivateKeyDer,
Attribute::OAuth2JwtLegacyCryptoEnable,
Attribute::OAuth2PreferShortUsername,
Attribute::OAuth2RsOriginLanding,
Attribute::Image,
Attribute::OAuth2RsClaimMap,
Attribute::OAuth2Session,
],
systemmust: vec![
Attribute::OAuth2RsOrigin,
Attribute::OAuth2RsTokenKey,
Attribute::Es256PrivateKeyDer,
],
..Default::default()
};
pub static ref SCHEMA_CLASS_OAUTH2_RS_DL7: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS,
name: EntryClass::OAuth2ResourceServer.into(),
description: "The class representing a configured OAuth2 Client".to_string(),
systemmay: vec![
Attribute::Description,
Attribute::OAuth2RsScopeMap,
Attribute::OAuth2RsSupScopeMap,
Attribute::Rs256PrivateKeyDer,
Attribute::OAuth2JwtLegacyCryptoEnable,
Attribute::OAuth2PreferShortUsername,
Attribute::Image,
Attribute::OAuth2RsClaimMap,
Attribute::OAuth2Session,
Attribute::OAuth2RsOrigin,
Attribute::OAuth2StrictRedirectUri,
],
systemmust: vec![
Attribute::OAuth2RsOriginLanding,
Attribute::OAuth2RsTokenKey,
Attribute::Es256PrivateKeyDer,
],
..Default::default()
};
pub static ref SCHEMA_CLASS_OAUTH2_RS_DL9: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS,
name: EntryClass::OAuth2ResourceServer.into(),

View file

@ -2,6 +2,7 @@
use crate::constants::entries::{Attribute, EntryClass};
use crate::constants::uuids::*;
use crate::schema::{SchemaAttribute, SchemaClass};
use crate::value::IndexType;
use crate::value::SyntaxType;
lazy_static!(
@ -10,6 +11,8 @@ pub static ref SCHEMA_ATTR_DISPLAYNAME: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DISPLAYNAME,
name: Attribute::DisplayName,
description: "The publicly visible display name of this person".to_string(),
index: vec![IndexType::Equality],
sync_allowed: true,
syntax: SyntaxType::Utf8String,
..Default::default()
@ -19,6 +22,8 @@ pub static ref SCHEMA_ATTR_DISPLAYNAME_DL7: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DISPLAYNAME,
name: Attribute::DisplayName,
description: "The publicly visible display name of this person".to_string(),
index: vec![IndexType::Equality, IndexType::SubString],
sync_allowed: true,
syntax: SyntaxType::Utf8String,
..Default::default()
@ -28,6 +33,8 @@ pub static ref SCHEMA_ATTR_MAIL: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_MAIL,
name: Attribute::Mail,
description: "Mail addresses of the object".to_string(),
index: vec![IndexType::Equality],
unique: true,
multivalue: true,
sync_allowed: true,
@ -39,6 +46,8 @@ pub static ref SCHEMA_ATTR_MAIL_DL7: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_MAIL,
name: Attribute::Mail,
description: "Mail addresses of the object".to_string(),
index: vec![IndexType::Equality, IndexType::SubString],
unique: true,
multivalue: true,
sync_allowed: true,
@ -50,6 +59,8 @@ pub static ref SCHEMA_ATTR_EC_KEY_PRIVATE: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_EC_KEY_PRIVATE,
name: Attribute::IdVerificationEcKey,
description: "Account verification private key".to_string(),
index: vec![IndexType::Presence],
unique: false,
sync_allowed: false,
syntax: SyntaxType::EcKeyPrivate,
@ -71,6 +82,8 @@ pub static ref SCHEMA_ATTR_PRIMARY_CREDENTIAL: SchemaAttribute = SchemaAttribute
uuid: UUID_SCHEMA_ATTR_PRIMARY_CREDENTIAL,
name: Attribute::PrimaryCredential,
description: "Primary credential material of the account for authentication interactively".to_string(),
index: vec![IndexType::Presence],
sync_allowed: true,
syntax: SyntaxType::Credential,
..Default::default()
@ -80,6 +93,8 @@ pub static ref SCHEMA_ATTR_LEGALNAME: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_LEGALNAME,
name: Attribute::LegalName,
description: "The private and sensitive legal name of this person".to_string(),
index: vec![IndexType::Equality],
sync_allowed: true,
syntax: SyntaxType::Utf8String,
..Default::default()
@ -89,6 +104,8 @@ pub static ref SCHEMA_ATTR_LEGALNAME_DL7: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_LEGALNAME,
name: Attribute::LegalName,
description: "The private and sensitive legal name of this person".to_string(),
index: vec![IndexType::Equality, IndexType::SubString],
sync_allowed: true,
syntax: SyntaxType::Utf8String,
..Default::default()
@ -98,6 +115,8 @@ pub static ref SCHEMA_ATTR_NAME_HISTORY: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_NAME_HISTORY,
name: Attribute::NameHistory,
description: "The history of names that a person has had".to_string(),
index: vec![IndexType::Equality],
multivalue: true,
sync_allowed: true,
syntax: SyntaxType::AuditLogString,
@ -118,6 +137,8 @@ pub static ref SCHEMA_ATTR_DOMAIN_NAME: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DOMAIN_NAME,
name: Attribute::DomainName,
description: "The domain's DNS name for webauthn and SPN generation purposes".to_string(),
index: vec![IndexType::Equality, IndexType::Presence],
unique: true,
syntax: SyntaxType::Utf8StringIname,
..Default::default()
@ -147,6 +168,8 @@ pub static ref SCHEMA_ATTR_DOMAIN_DISPLAY_NAME: SchemaAttribute = SchemaAttribut
uuid: UUID_SCHEMA_ATTR_DOMAIN_DISPLAY_NAME,
name: Attribute::DomainDisplayName,
description: "The user-facing display name of the Kanidm domain".to_string(),
index: vec![IndexType::Equality],
syntax: SyntaxType::Utf8String,
..Default::default()
};
@ -155,6 +178,8 @@ pub static ref SCHEMA_ATTR_DOMAIN_UUID: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DOMAIN_UUID,
name: Attribute::DomainUuid,
description: "The domain's uuid, used in CSN and trust relationships".to_string(),
index: vec![IndexType::Equality],
unique: true,
syntax: SyntaxType::Uuid,
..Default::default()
@ -164,6 +189,8 @@ pub static ref SCHEMA_ATTR_DOMAIN_SSID: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DOMAIN_SSID,
name: Attribute::DomainSsid,
description: "The domains site-wide SSID for device autoconfiguration of wireless".to_string(),
index: vec![IndexType::Equality],
unique: true,
syntax: SyntaxType::Utf8String,
..Default::default()
@ -210,6 +237,8 @@ pub static ref SCHEMA_ATTR_GIDNUMBER: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_GIDNUMBER,
name: Attribute::GidNumber,
description: "The groupid (uid) number of a group or account.to_string(). This is the same value as the UID number on posix accounts for security reasons".to_string(),
index: vec![IndexType::Equality],
unique: true,
sync_allowed: true,
syntax: SyntaxType::Uint32,
@ -267,6 +296,8 @@ pub static ref SCHEMA_ATTR_UNIX_PASSWORD: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_UNIX_PASSWORD,
name: Attribute::UnixPassword,
description: "A POSIX user's UNIX login password".to_string(),
index: vec![IndexType::Presence],
syntax: SyntaxType::Credential,
..Default::default()
};
@ -275,6 +306,8 @@ pub static ref SCHEMA_ATTR_NSUNIQUEID: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_NSUNIQUEID,
name: Attribute::NsUniqueId,
description: "A unique id compatibility for 389-ds/dsee".to_string(),
index: vec![IndexType::Equality],
unique: true,
sync_allowed: true,
syntax: SyntaxType::NsUniqueId,
@ -315,6 +348,8 @@ pub static ref SCHEMA_ATTR_OAUTH2_RS_NAME: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_NAME,
name: Attribute::OAuth2RsName,
description: "The unique name of an external Oauth2 resource".to_string(),
index: vec![IndexType::Equality],
unique: true,
syntax: SyntaxType::Utf8StringIname,
..Default::default()
@ -362,6 +397,8 @@ pub static ref SCHEMA_ATTR_OAUTH2_RS_CLAIM_MAP_DL4: SchemaAttribute = SchemaAttr
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_CLAIM_MAP,
name: Attribute::OAuth2RsClaimMap,
description: "A set of custom claims mapped to group memberships of accounts".to_string(),
index: vec![IndexType::Equality],
multivalue: true,
// CHANGE ME
syntax: SyntaxType::OauthClaimMap,
@ -372,6 +409,8 @@ pub static ref SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP: SchemaAttribute = SchemaAttribut
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP,
name: Attribute::OAuth2RsScopeMap,
description: "A reference to a group mapped to scopes for the associated oauth2 resource server".to_string(),
index: vec![IndexType::Equality],
multivalue: true,
syntax: SyntaxType::OauthScopeMap,
..Default::default()
@ -381,6 +420,8 @@ pub static ref SCHEMA_ATTR_OAUTH2_RS_SUP_SCOPE_MAP: SchemaAttribute = SchemaAttr
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_SUP_SCOPE_MAP,
name: Attribute::OAuth2RsSupScopeMap,
description: "A reference to a group mapped to scopes for the associated oauth2 resource server".to_string(),
index: vec![IndexType::Equality],
multivalue: true,
syntax: SyntaxType::OauthScopeMap,
..Default::default()
@ -418,6 +459,8 @@ pub static ref SCHEMA_ATTR_OAUTH2_CONSENT_SCOPE_MAP: SchemaAttribute = SchemaAtt
uuid: UUID_SCHEMA_ATTR_OAUTH2_CONSENT_SCOPE_MAP,
name: Attribute::OAuth2ConsentScopeMap,
description: "A set of scopes mapped from a relying server to a user, where the user has previously consented to the following. If changed or deleted, consent will be re-sought".to_string(),
index: vec![IndexType::Equality],
multivalue: true,
syntax: SyntaxType::OauthScopeMap,
..Default::default()
@ -464,6 +507,8 @@ pub static ref SCHEMA_ATTR_JWS_ES256_PRIVATE_KEY: SchemaAttribute = SchemaAttrib
uuid: UUID_SCHEMA_ATTR_JWS_ES256_PRIVATE_KEY,
name: Attribute::JwsEs256PrivateKey,
description: "An es256 private key for jws".to_string(),
index: vec![IndexType::Equality],
unique: true,
syntax: SyntaxType::JwsKeyEs256,
..Default::default()
@ -501,6 +546,8 @@ pub static ref SCHEMA_ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN: SchemaAttribute = Sch
uuid: UUID_SCHEMA_ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN,
name: Attribute::CredentialUpdateIntentToken,
description: "The status of a credential update intent token".to_string(),
index: vec![IndexType::Equality],
multivalue: true,
syntax: SyntaxType::IntentToken,
..Default::default()
@ -510,6 +557,8 @@ pub static ref SCHEMA_ATTR_PASSKEYS: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_PASSKEYS,
name: Attribute::PassKeys,
description: "A set of registered passkeys".to_string(),
index: vec![IndexType::Equality],
multivalue: true,
sync_allowed: true,
syntax: SyntaxType::Passkey,
@ -520,6 +569,8 @@ pub static ref SCHEMA_ATTR_ATTESTED_PASSKEYS: SchemaAttribute = SchemaAttribute
uuid: UUID_SCHEMA_ATTR_ATTESTED_PASSKEYS,
name: Attribute::AttestedPasskeys,
description: "A set of registered device keys".to_string(),
index: vec![IndexType::Equality],
multivalue: true,
sync_allowed: true,
syntax: SyntaxType::AttestedPasskey,
@ -548,6 +599,8 @@ pub static ref SCHEMA_ATTR_API_TOKEN_SESSION: SchemaAttribute = SchemaAttribute
uuid: UUID_SCHEMA_ATTR_API_TOKEN_SESSION,
name: Attribute::ApiTokenSession,
description: "A session entry related to an issued API token".to_string(),
index: vec![IndexType::Equality],
unique: true,
multivalue: true,
syntax: SyntaxType::ApiToken,
@ -558,6 +611,8 @@ pub static ref SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION: SchemaAttribute = SchemaAttr
uuid: UUID_SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION,
name: Attribute::UserAuthTokenSession,
description: "A session entry related to an issued user auth token".to_string(),
index: vec![IndexType::Equality],
unique: true,
multivalue: true,
syntax: SyntaxType::Session,
@ -568,6 +623,8 @@ pub static ref SCHEMA_ATTR_OAUTH2_SESSION: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_OAUTH2_SESSION,
name: Attribute::OAuth2Session,
description: "A session entry to an active oauth2 session, bound to a parent user auth token".to_string(),
index: vec![IndexType::Equality],
multivalue: true,
syntax: SyntaxType::Oauth2Session,
..Default::default()
@ -577,6 +634,8 @@ pub static ref SCHEMA_ATTR_SYNC_TOKEN_SESSION: SchemaAttribute = SchemaAttribute
uuid: UUID_SCHEMA_ATTR_SYNC_TOKEN_SESSION,
name: Attribute::SyncTokenSession,
description: "A session entry related to an issued sync token".to_string(),
index: vec![IndexType::Equality],
unique: true,
syntax: SyntaxType::ApiToken,
..Default::default()
@ -595,6 +654,8 @@ pub static ref SCHEMA_ATTR_GRANT_UI_HINT: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_GRANT_UI_HINT,
name: Attribute::GrantUiHint,
description: "A UI hint that is granted via membership to a group".to_string(),
index: vec![IndexType::Equality],
multivalue: true,
syntax: SyntaxType::UiHint,
..Default::default()

View file

@ -2,6 +2,7 @@
use crate::constants::entries::{Attribute, EntryClass};
use crate::constants::uuids::*;
use crate::schema::{SchemaAttribute, SchemaClass};
use crate::value::IndexType;
use crate::value::SyntaxType;
lazy_static!(
@ -10,6 +11,8 @@ pub static ref SCHEMA_ATTR_DISPLAYNAME: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DISPLAYNAME,
name: Attribute::DisplayName,
description: "The publicly visible display name of this person".to_string(),
index: vec![IndexType::Equality],
sync_allowed: true,
syntax: SyntaxType::Utf8String,
..Default::default()
@ -19,6 +22,8 @@ pub static ref SCHEMA_ATTR_DISPLAYNAME_DL7: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DISPLAYNAME,
name: Attribute::DisplayName,
description: "The publicly visible display name of this person".to_string(),
index: vec![IndexType::Equality, IndexType::SubString],
sync_allowed: true,
syntax: SyntaxType::Utf8String,
..Default::default()
@ -28,6 +33,8 @@ pub static ref SCHEMA_ATTR_MAIL: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_MAIL,
name: Attribute::Mail,
description: "Mail addresses of the object".to_string(),
index: vec![IndexType::Equality],
unique: true,
multivalue: true,
sync_allowed: true,
@ -39,6 +46,8 @@ pub static ref SCHEMA_ATTR_MAIL_DL7: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_MAIL,
name: Attribute::Mail,
description: "Mail addresses of the object".to_string(),
index: vec![IndexType::Equality, IndexType::SubString],
unique: true,
multivalue: true,
sync_allowed: true,
@ -50,6 +59,8 @@ pub static ref SCHEMA_ATTR_EC_KEY_PRIVATE: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_EC_KEY_PRIVATE,
name: Attribute::IdVerificationEcKey,
description: "Account verification private key".to_string(),
index: vec![IndexType::Presence],
unique: false,
sync_allowed: false,
syntax: SyntaxType::EcKeyPrivate,
@ -71,6 +82,8 @@ pub static ref SCHEMA_ATTR_PRIMARY_CREDENTIAL: SchemaAttribute = SchemaAttribute
uuid: UUID_SCHEMA_ATTR_PRIMARY_CREDENTIAL,
name: Attribute::PrimaryCredential,
description: "Primary credential material of the account for authentication interactively".to_string(),
index: vec![IndexType::Presence],
sync_allowed: true,
syntax: SyntaxType::Credential,
..Default::default()
@ -80,6 +93,8 @@ pub static ref SCHEMA_ATTR_LEGALNAME: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_LEGALNAME,
name: Attribute::LegalName,
description: "The private and sensitive legal name of this person".to_string(),
index: vec![IndexType::Equality],
sync_allowed: true,
syntax: SyntaxType::Utf8String,
..Default::default()
@ -89,6 +104,8 @@ pub static ref SCHEMA_ATTR_LEGALNAME_DL7: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_LEGALNAME,
name: Attribute::LegalName,
description: "The private and sensitive legal name of this person".to_string(),
index: vec![IndexType::Equality, IndexType::SubString],
sync_allowed: true,
syntax: SyntaxType::Utf8String,
..Default::default()
@ -98,6 +115,8 @@ pub static ref SCHEMA_ATTR_NAME_HISTORY: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_NAME_HISTORY,
name: Attribute::NameHistory,
description: "The history of names that a person has had".to_string(),
index: vec![IndexType::Equality],
multivalue: true,
sync_allowed: true,
syntax: SyntaxType::AuditLogString,
@ -118,6 +137,8 @@ pub static ref SCHEMA_ATTR_DOMAIN_NAME: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DOMAIN_NAME,
name: Attribute::DomainName,
description: "The domain's DNS name for webauthn and SPN generation purposes".to_string(),
index: vec![IndexType::Equality, IndexType::Presence],
unique: true,
syntax: SyntaxType::Utf8StringIname,
..Default::default()
@ -147,6 +168,8 @@ pub static ref SCHEMA_ATTR_DOMAIN_DISPLAY_NAME: SchemaAttribute = SchemaAttribut
uuid: UUID_SCHEMA_ATTR_DOMAIN_DISPLAY_NAME,
name: Attribute::DomainDisplayName,
description: "The user-facing display name of the Kanidm domain".to_string(),
index: vec![IndexType::Equality],
syntax: SyntaxType::Utf8String,
..Default::default()
};
@ -155,6 +178,8 @@ pub static ref SCHEMA_ATTR_DOMAIN_UUID: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DOMAIN_UUID,
name: Attribute::DomainUuid,
description: "The domain's uuid, used in CSN and trust relationships".to_string(),
index: vec![IndexType::Equality],
unique: true,
syntax: SyntaxType::Uuid,
..Default::default()
@ -164,6 +189,8 @@ pub static ref SCHEMA_ATTR_DOMAIN_SSID: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DOMAIN_SSID,
name: Attribute::DomainSsid,
description: "The domains site-wide SSID for device autoconfiguration of wireless".to_string(),
index: vec![IndexType::Equality],
unique: true,
syntax: SyntaxType::Utf8String,
..Default::default()
@ -210,6 +237,8 @@ pub static ref SCHEMA_ATTR_GIDNUMBER: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_GIDNUMBER,
name: Attribute::GidNumber,
description: "The groupid (uid) number of a group or account.to_string(). This is the same value as the UID number on posix accounts for security reasons".to_string(),
index: vec![IndexType::Equality],
unique: true,
sync_allowed: true,
syntax: SyntaxType::Uint32,
@ -267,6 +296,8 @@ pub static ref SCHEMA_ATTR_UNIX_PASSWORD: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_UNIX_PASSWORD,
name: Attribute::UnixPassword,
description: "A POSIX user's UNIX login password".to_string(),
index: vec![IndexType::Presence],
syntax: SyntaxType::Credential,
..Default::default()
};
@ -275,6 +306,8 @@ pub static ref SCHEMA_ATTR_NSUNIQUEID: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_NSUNIQUEID,
name: Attribute::NsUniqueId,
description: "A unique id compatibility for 389-ds/dsee".to_string(),
index: vec![IndexType::Equality],
unique: true,
sync_allowed: true,
syntax: SyntaxType::NsUniqueId,
@ -315,6 +348,8 @@ pub static ref SCHEMA_ATTR_OAUTH2_RS_NAME: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_NAME,
name: Attribute::OAuth2RsName,
description: "The unique name of an external Oauth2 resource".to_string(),
index: vec![IndexType::Equality],
unique: true,
syntax: SyntaxType::Utf8StringIname,
..Default::default()
@ -362,6 +397,8 @@ pub static ref SCHEMA_ATTR_OAUTH2_RS_CLAIM_MAP_DL4: SchemaAttribute = SchemaAttr
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_CLAIM_MAP,
name: Attribute::OAuth2RsClaimMap,
description: "A set of custom claims mapped to group memberships of accounts".to_string(),
index: vec![IndexType::Equality],
multivalue: true,
// CHANGE ME
syntax: SyntaxType::OauthClaimMap,
@ -372,6 +409,8 @@ pub static ref SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP: SchemaAttribute = SchemaAttribut
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP,
name: Attribute::OAuth2RsScopeMap,
description: "A reference to a group mapped to scopes for the associated oauth2 resource server".to_string(),
index: vec![IndexType::Equality],
multivalue: true,
syntax: SyntaxType::OauthScopeMap,
..Default::default()
@ -381,6 +420,8 @@ pub static ref SCHEMA_ATTR_OAUTH2_RS_SUP_SCOPE_MAP: SchemaAttribute = SchemaAttr
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_SUP_SCOPE_MAP,
name: Attribute::OAuth2RsSupScopeMap,
description: "A reference to a group mapped to scopes for the associated oauth2 resource server".to_string(),
index: vec![IndexType::Equality],
multivalue: true,
syntax: SyntaxType::OauthScopeMap,
..Default::default()
@ -418,6 +459,8 @@ pub static ref SCHEMA_ATTR_OAUTH2_CONSENT_SCOPE_MAP: SchemaAttribute = SchemaAtt
uuid: UUID_SCHEMA_ATTR_OAUTH2_CONSENT_SCOPE_MAP,
name: Attribute::OAuth2ConsentScopeMap,
description: "A set of scopes mapped from a relying server to a user, where the user has previously consented to the following. If changed or deleted, consent will be re-sought".to_string(),
index: vec![IndexType::Equality],
multivalue: true,
syntax: SyntaxType::OauthScopeMap,
..Default::default()
@ -464,6 +507,8 @@ pub static ref SCHEMA_ATTR_JWS_ES256_PRIVATE_KEY: SchemaAttribute = SchemaAttrib
uuid: UUID_SCHEMA_ATTR_JWS_ES256_PRIVATE_KEY,
name: Attribute::JwsEs256PrivateKey,
description: "An es256 private key for jws".to_string(),
index: vec![IndexType::Equality],
unique: true,
syntax: SyntaxType::JwsKeyEs256,
..Default::default()
@ -501,6 +546,8 @@ pub static ref SCHEMA_ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN: SchemaAttribute = Sch
uuid: UUID_SCHEMA_ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN,
name: Attribute::CredentialUpdateIntentToken,
description: "The status of a credential update intent token".to_string(),
index: vec![IndexType::Equality],
multivalue: true,
syntax: SyntaxType::IntentToken,
..Default::default()
@ -510,6 +557,8 @@ pub static ref SCHEMA_ATTR_PASSKEYS: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_PASSKEYS,
name: Attribute::PassKeys,
description: "A set of registered passkeys".to_string(),
index: vec![IndexType::Equality],
multivalue: true,
sync_allowed: true,
syntax: SyntaxType::Passkey,
@ -520,6 +569,8 @@ pub static ref SCHEMA_ATTR_ATTESTED_PASSKEYS: SchemaAttribute = SchemaAttribute
uuid: UUID_SCHEMA_ATTR_ATTESTED_PASSKEYS,
name: Attribute::AttestedPasskeys,
description: "A set of registered device keys".to_string(),
index: vec![IndexType::Equality],
multivalue: true,
sync_allowed: true,
syntax: SyntaxType::AttestedPasskey,
@ -548,6 +599,8 @@ pub static ref SCHEMA_ATTR_API_TOKEN_SESSION: SchemaAttribute = SchemaAttribute
uuid: UUID_SCHEMA_ATTR_API_TOKEN_SESSION,
name: Attribute::ApiTokenSession,
description: "A session entry related to an issued API token".to_string(),
index: vec![IndexType::Equality],
unique: true,
multivalue: true,
syntax: SyntaxType::ApiToken,
@ -558,6 +611,8 @@ pub static ref SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION: SchemaAttribute = SchemaAttr
uuid: UUID_SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION,
name: Attribute::UserAuthTokenSession,
description: "A session entry related to an issued user auth token".to_string(),
index: vec![IndexType::Equality],
unique: true,
multivalue: true,
syntax: SyntaxType::Session,
@ -568,6 +623,8 @@ pub static ref SCHEMA_ATTR_OAUTH2_SESSION: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_OAUTH2_SESSION,
name: Attribute::OAuth2Session,
description: "A session entry to an active oauth2 session, bound to a parent user auth token".to_string(),
index: vec![IndexType::Equality],
multivalue: true,
syntax: SyntaxType::Oauth2Session,
..Default::default()
@ -577,6 +634,8 @@ pub static ref SCHEMA_ATTR_SYNC_TOKEN_SESSION: SchemaAttribute = SchemaAttribute
uuid: UUID_SCHEMA_ATTR_SYNC_TOKEN_SESSION,
name: Attribute::SyncTokenSession,
description: "A session entry related to an issued sync token".to_string(),
index: vec![IndexType::Equality],
unique: true,
syntax: SyntaxType::ApiToken,
..Default::default()
@ -595,6 +654,8 @@ pub static ref SCHEMA_ATTR_GRANT_UI_HINT: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_GRANT_UI_HINT,
name: Attribute::GrantUiHint,
description: "A UI hint that is granted via membership to a group".to_string(),
index: vec![IndexType::Equality],
multivalue: true,
syntax: SyntaxType::UiHint,
..Default::default()

View file

@ -695,6 +695,7 @@ mod tests {
let e = entry_init!(
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(Attribute::DisplayName, Value::new_iname("testperson")),
(
@ -725,6 +726,7 @@ mod tests {
let e = entry_init!(
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(Attribute::DisplayName, Value::new_iname("testperson")),
(

View file

@ -109,7 +109,7 @@ impl DynGroup {
nd_group.purge_ava(Attribute::DynMember);
}
// Insert it to the dyngroup cache with the parsed filter for
// Insert it to the dyngroup cache with the compiled/resolved filter for
// fast matching in other paths.
if dyn_groups.insts.insert(uuid, scope_i).is_none() == expect {
error!("{} cache uuid conflict {}", Attribute::DynGroup, uuid);
@ -175,11 +175,6 @@ impl DynGroup {
) -> Result<BTreeSet<Uuid>, OperationError> {
let mut affected_uuids = BTreeSet::new();
if qs.get_phase() < ServerPhase::SchemaReady {
debug!("Server is not ready to apply dyngroups");
return Ok(affected_uuids);
}
let ident_internal = Identity::from_internal();
let (n_dyn_groups, entries): (Vec<&Entry<_, _>>, Vec<_>) = cand.iter().partition(|entry| {
@ -207,7 +202,9 @@ impl DynGroup {
let dg_filter_valid = dg_filter
.validate(qs.get_schema())
.map_err(OperationError::SchemaViolation)
.and_then(|f| f.resolve(&ident_internal, None, qs.get_resolve_filter_cache()))?;
.and_then(|f| {
f.resolve(&ident_internal, None, Some(qs.get_resolve_filter_cache()))
})?;
// Did any of our modified entries match our dyn group filter?
let matches: Vec<_> = entries
@ -294,11 +291,6 @@ impl DynGroup {
) -> Result<BTreeSet<Uuid>, OperationError> {
let mut affected_uuids = BTreeSet::new();
if qs.get_phase() < ServerPhase::SchemaReady {
debug!("Server is not ready to apply dyngroups");
return Ok(affected_uuids);
}
let ident_internal = Identity::from_internal();
// Probably should be filter here instead.
@ -346,7 +338,9 @@ impl DynGroup {
let dg_filter_valid = dg_filter
.validate(qs.get_schema())
.map_err(OperationError::SchemaViolation)
.and_then(|f| f.resolve(&ident_internal, None, qs.get_resolve_filter_cache()))?;
.and_then(|f| {
f.resolve(&ident_internal, None, Some(qs.get_resolve_filter_cache()))
})?;
let matches: Vec<_> = pre_entries
.iter()

View file

@ -22,6 +22,7 @@ mod jwskeygen;
mod keyobject;
mod memberof;
mod namehistory;
mod protected;
mod refint;
mod session;
mod spn;
@ -43,7 +44,6 @@ trait Plugin {
Err(OperationError::InvalidState)
}
#[allow(dead_code)]
fn pre_create(
_qs: &mut QueryServerWriteTransaction,
// List of what we will commit that is valid?
@ -243,13 +243,13 @@ impl Plugins {
attrunique::AttrUnique::pre_create_transform(qs, cand, ce)
}
#[instrument(level = "trace", name = "plugins::run_pre_create", skip_all)]
#[instrument(level = "debug", name = "plugins::run_pre_create", skip_all)]
pub fn run_pre_create(
_qs: &mut QueryServerWriteTransaction,
_cand: &[Entry<EntrySealed, EntryNew>],
_ce: &CreateEvent,
qs: &mut QueryServerWriteTransaction,
cand: &[Entry<EntrySealed, EntryNew>],
ce: &CreateEvent,
) -> Result<(), OperationError> {
Ok(())
protected::Protected::pre_create(qs, cand, ce)
}
#[instrument(level = "debug", name = "plugins::run_post_create", skip_all)]
@ -269,6 +269,7 @@ impl Plugins {
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
me: &ModifyEvent,
) -> Result<(), OperationError> {
protected::Protected::pre_modify(qs, pre_cand, cand, me)?;
base::Base::pre_modify(qs, pre_cand, cand, me)?;
valuedeny::ValueDeny::pre_modify(qs, pre_cand, cand, me)?;
cred_import::CredImport::pre_modify(qs, pre_cand, cand, me)?;
@ -304,6 +305,7 @@ impl Plugins {
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
me: &BatchModifyEvent,
) -> Result<(), OperationError> {
protected::Protected::pre_batch_modify(qs, pre_cand, cand, me)?;
base::Base::pre_batch_modify(qs, pre_cand, cand, me)?;
valuedeny::ValueDeny::pre_batch_modify(qs, pre_cand, cand, me)?;
cred_import::CredImport::pre_batch_modify(qs, pre_cand, cand, me)?;
@ -338,6 +340,7 @@ impl Plugins {
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
de: &DeleteEvent,
) -> Result<(), OperationError> {
protected::Protected::pre_delete(qs, cand, de)?;
memberof::MemberOf::pre_delete(qs, cand, de)
}

View file

@ -0,0 +1,690 @@
// System protected objects. Items matching specific requirements
// may only have certain modifications performed.
use hashbrown::HashSet;
use std::sync::Arc;
use crate::event::{CreateEvent, DeleteEvent, ModifyEvent};
use crate::modify::Modify;
use crate::plugins::Plugin;
use crate::prelude::*;
pub struct Protected {}
// Here is the declaration of all the attrs that can be altered by
// a call on a system object. We trust they are allowed because
// schema will have checked this, and we don't allow class changes!
lazy_static! {
static ref ALLOWED_ATTRS: HashSet<Attribute> = {
let attrs = vec![
// Allow modification of some schema class types to allow local extension
// of schema types.
Attribute::Must,
Attribute::May,
// modification of some domain info types for local configuratiomn.
Attribute::DomainSsid,
Attribute::DomainLdapBasedn,
Attribute::LdapMaxQueryableAttrs,
Attribute::LdapAllowUnixPwBind,
Attribute::FernetPrivateKeyStr,
Attribute::Es256PrivateKeyDer,
Attribute::KeyActionRevoke,
Attribute::KeyActionRotate,
Attribute::IdVerificationEcKey,
Attribute::BadlistPassword,
Attribute::DeniedName,
Attribute::DomainDisplayName,
Attribute::Image,
// modification of account policy values for dyngroup.
Attribute::AuthSessionExpiry,
Attribute::AuthPasswordMinimumLength,
Attribute::CredentialTypeMinimum,
Attribute::PrivilegeExpiry,
Attribute::WebauthnAttestationCaList,
Attribute::LimitSearchMaxResults,
Attribute::LimitSearchMaxFilterTest,
Attribute::AllowPrimaryCredFallback,
];
let mut m = HashSet::with_capacity(attrs.len());
m.extend(attrs);
m
};
static ref PROTECTED_ENTRYCLASSES: Vec<EntryClass> =
vec![
EntryClass::System,
EntryClass::DomainInfo,
EntryClass::SystemInfo,
EntryClass::SystemConfig,
EntryClass::DynGroup,
EntryClass::SyncObject,
EntryClass::Tombstone,
EntryClass::Recycled,
];
}
impl Plugin for Protected {
fn id() -> &'static str {
"plugin_protected"
}
#[instrument(level = "debug", name = "protected_pre_create", skip_all)]
fn pre_create(
_qs: &mut QueryServerWriteTransaction,
// List of what we will commit that is valid?
cand: &[Entry<EntrySealed, EntryNew>],
ce: &CreateEvent,
) -> Result<(), OperationError> {
if ce.ident.is_internal() {
trace!("Internal operation, not enforcing system object protection");
return Ok(());
}
cand.iter().try_fold((), |(), cand| {
if PROTECTED_ENTRYCLASSES
.iter()
.any(|c| cand.attribute_equality(Attribute::Class, &c.to_partialvalue()))
{
trace!("Rejecting operation during pre_create check");
Err(OperationError::SystemProtectedObject)
} else {
Ok(())
}
})
}
#[instrument(level = "debug", name = "protected_pre_modify", skip_all)]
fn pre_modify(
_qs: &mut QueryServerWriteTransaction,
_pre_cand: &[Arc<EntrySealedCommitted>],
cand: &mut Vec<EntryInvalidCommitted>,
me: &ModifyEvent,
) -> Result<(), OperationError> {
if me.ident.is_internal() {
trace!("Internal operation, not enforcing system object protection");
return Ok(());
}
// Prevent adding class: system, domain_info, tombstone, or recycled.
me.modlist.iter().try_fold((), |(), m| match m {
Modify::Present(a, v) => {
if a == Attribute::Class.as_ref()
&& PROTECTED_ENTRYCLASSES.iter().any(|c| v == &c.to_value())
{
trace!("Rejecting operation during pre_modify check");
Err(OperationError::SystemProtectedObject)
} else {
Ok(())
}
}
_ => Ok(()),
})?;
// HARD block mods on tombstone or recycle. We soft block on the rest as they may
// have some allowed attrs.
cand.iter().try_fold((), |(), cand| {
if cand.attribute_equality(Attribute::Class, &EntryClass::Tombstone.into())
|| cand.attribute_equality(Attribute::Class, &EntryClass::Recycled.into())
{
Err(OperationError::SystemProtectedObject)
} else {
Ok(())
}
})?;
// if class: system, check the mods are "allowed"
let system_pres = cand.iter().any(|c| {
// We don't need to check for domain info here because domain_info has a class
// system also. We just need to block it from being created.
c.attribute_equality(Attribute::Class, &EntryClass::System.into())
});
trace!("class: system -> {}", system_pres);
// No system types being altered, return.
if !system_pres {
return Ok(());
}
// Something altered is system, check if it's allowed.
me.modlist.into_iter().try_fold((), |(), m| {
// Already hit an error, move on.
let a = match m {
Modify::Present(a, _)
| Modify::Removed(a, _)
| Modify::Set(a, _)
| Modify::Purged(a) => Some(a),
Modify::Assert(_, _) => None,
};
if let Some(attr) = a {
match ALLOWED_ATTRS.contains(attr) {
true => Ok(()),
false => {
trace!("If you're getting this, you need to modify the ALLOWED_ATTRS list");
Err(OperationError::SystemProtectedObject)
}
}
} else {
// Was not a mod needing checking
Ok(())
}
})
}
#[instrument(level = "debug", name = "protected_pre_batch_modify", skip_all)]
fn pre_batch_modify(
_qs: &mut QueryServerWriteTransaction,
_pre_cand: &[Arc<EntrySealedCommitted>],
cand: &mut Vec<EntryInvalidCommitted>,
me: &BatchModifyEvent,
) -> Result<(), OperationError> {
if me.ident.is_internal() {
trace!("Internal operation, not enforcing system object protection");
return Ok(());
}
me.modset
.values()
.flat_map(|ml| ml.iter())
.try_fold((), |(), m| match m {
Modify::Present(a, v) => {
if a == Attribute::Class.as_ref()
&& PROTECTED_ENTRYCLASSES.iter().any(|c| v == &c.to_value())
{
trace!("Rejecting operation during pre_batch_modify check");
Err(OperationError::SystemProtectedObject)
} else {
Ok(())
}
}
_ => Ok(()),
})?;
// HARD block mods on tombstone or recycle. We soft block on the rest as they may
// have some allowed attrs.
cand.iter().try_fold((), |(), cand| {
if cand.attribute_equality(Attribute::Class, &EntryClass::Tombstone.into())
|| cand.attribute_equality(Attribute::Class, &EntryClass::Recycled.into())
{
Err(OperationError::SystemProtectedObject)
} else {
Ok(())
}
})?;
// if class: system, check the mods are "allowed"
let system_pres = cand.iter().any(|c| {
// We don't need to check for domain info here because domain_info has a class
// system also. We just need to block it from being created.
c.attribute_equality(Attribute::Class, &EntryClass::System.into())
});
trace!("{}: system -> {}", Attribute::Class, system_pres);
// No system types being altered, return.
if !system_pres {
return Ok(());
}
// Something altered is system, check if it's allowed.
me.modset
.values()
.flat_map(|ml| ml.iter())
.try_fold((), |(), m| {
// Already hit an error, move on.
let a = match m {
Modify::Present(a, _) | Modify::Removed(a, _) | Modify::Set(a, _) | Modify::Purged(a) => Some(a),
Modify::Assert(_, _) => None,
};
if let Some(attr) = a {
match ALLOWED_ATTRS.contains(attr) {
true => Ok(()),
false => {
trace!("Rejecting operation during pre_batch_modify check, if you're getting this check ALLOWED_ATTRS");
Err(OperationError::SystemProtectedObject)
},
}
} else {
// Was not a mod needing checking
Ok(())
}
})
}
#[instrument(level = "debug", name = "protected_pre_delete", skip_all)]
fn pre_delete(
_qs: &mut QueryServerWriteTransaction,
// Should these be EntrySealed
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
de: &DeleteEvent,
) -> Result<(), OperationError> {
if de.ident.is_internal() {
trace!("Internal operation, not enforcing system object protection");
return Ok(());
}
cand.iter().try_fold((), |(), cand| {
if PROTECTED_ENTRYCLASSES
.iter()
.any(|c| cand.attribute_equality(Attribute::Class, &c.to_partialvalue()))
{
trace!("Rejecting operation during pre_delete check");
Err(OperationError::SystemProtectedObject)
} else {
Ok(())
}
})
}
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
use std::sync::Arc;
const UUID_TEST_ACCOUNT: Uuid = uuid::uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
const UUID_TEST_GROUP: Uuid = uuid::uuid!("81ec1640-3637-4a2f-8a52-874fa3c3c92f");
const UUID_TEST_ACP: Uuid = uuid::uuid!("acae81d6-5ea7-4bd8-8f7f-fcec4c0dd647");
lazy_static! {
pub static ref TEST_ACCOUNT: EntryInitNew = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
(Attribute::Class, EntryClass::MemberOf.to_value()),
(Attribute::Name, Value::new_iname("test_account_1")),
(Attribute::DisplayName, Value::new_utf8s("test_account_1")),
(Attribute::Uuid, Value::Uuid(UUID_TEST_ACCOUNT)),
(Attribute::MemberOf, Value::Refer(UUID_TEST_GROUP))
);
pub static ref TEST_GROUP: EntryInitNew = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("test_group_a")),
(Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP)),
(Attribute::Member, Value::Refer(UUID_TEST_ACCOUNT))
);
pub static ref ALLOW_ALL: EntryInitNew = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(
Attribute::Class,
EntryClass::AccessControlProfile.to_value()
),
(
Attribute::Class,
EntryClass::AccessControlTargetScope.to_value()
),
(
Attribute::Class,
EntryClass::AccessControlReceiverGroup.to_value()
),
(Attribute::Class, EntryClass::AccessControlModify.to_value()),
(Attribute::Class, EntryClass::AccessControlCreate.to_value()),
(Attribute::Class, EntryClass::AccessControlDelete.to_value()),
(Attribute::Class, EntryClass::AccessControlSearch.to_value()),
(
Attribute::Name,
Value::new_iname("idm_admins_acp_allow_all_test")
),
(Attribute::Uuid, Value::Uuid(UUID_TEST_ACP)),
(Attribute::AcpReceiverGroup, Value::Refer(UUID_TEST_GROUP)),
(
Attribute::AcpTargetScope,
Value::new_json_filter_s("{\"pres\":\"class\"}").expect("filter")
),
(Attribute::AcpSearchAttr, Value::from(Attribute::Name)),
(Attribute::AcpSearchAttr, Value::from(Attribute::Class)),
(Attribute::AcpSearchAttr, Value::from(Attribute::Uuid)),
(Attribute::AcpSearchAttr, Value::new_iutf8("classname")),
(
Attribute::AcpSearchAttr,
Value::new_iutf8(Attribute::AttributeName.as_ref())
),
(Attribute::AcpModifyClass, EntryClass::System.to_value()),
(Attribute::AcpModifyClass, Value::new_iutf8("domain_info")),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::Class)
),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::DisplayName)
),
(Attribute::AcpModifyRemovedAttr, Value::from(Attribute::May)),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::Must)
),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::DomainName)
),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::DomainDisplayName)
),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::DomainUuid)
),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::DomainSsid)
),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::FernetPrivateKeyStr)
),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::Es256PrivateKeyDer)
),
(
Attribute::AcpModifyRemovedAttr,
Value::from(Attribute::PrivateCookieKey)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::Class)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::DisplayName)
),
(Attribute::AcpModifyPresentAttr, Value::from(Attribute::May)),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::Must)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::DomainName)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::DomainDisplayName)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::DomainUuid)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::DomainSsid)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::FernetPrivateKeyStr)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::Es256PrivateKeyDer)
),
(
Attribute::AcpModifyPresentAttr,
Value::from(Attribute::PrivateCookieKey)
),
(Attribute::AcpCreateClass, EntryClass::Object.to_value()),
(Attribute::AcpCreateClass, EntryClass::Account.to_value()),
(Attribute::AcpCreateClass, EntryClass::Person.to_value()),
(Attribute::AcpCreateClass, EntryClass::System.to_value()),
(Attribute::AcpCreateClass, EntryClass::DomainInfo.to_value()),
(Attribute::AcpCreateAttr, Value::from(Attribute::Name)),
(Attribute::AcpCreateAttr, EntryClass::Class.to_value(),),
(
Attribute::AcpCreateAttr,
Value::from(Attribute::Description),
),
(
Attribute::AcpCreateAttr,
Value::from(Attribute::DisplayName),
),
(Attribute::AcpCreateAttr, Value::from(Attribute::DomainName),),
(
Attribute::AcpCreateAttr,
Value::from(Attribute::DomainDisplayName)
),
(Attribute::AcpCreateAttr, Value::from(Attribute::DomainUuid)),
(Attribute::AcpCreateAttr, Value::from(Attribute::DomainSsid)),
(Attribute::AcpCreateAttr, Value::from(Attribute::Uuid)),
(
Attribute::AcpCreateAttr,
Value::from(Attribute::FernetPrivateKeyStr)
),
(
Attribute::AcpCreateAttr,
Value::from(Attribute::Es256PrivateKeyDer)
),
(
Attribute::AcpCreateAttr,
Value::from(Attribute::PrivateCookieKey)
),
(Attribute::AcpCreateAttr, Value::from(Attribute::Version))
);
pub static ref PRELOAD: Vec<EntryInitNew> =
vec![TEST_ACCOUNT.clone(), TEST_GROUP.clone(), ALLOW_ALL.clone()];
pub static ref E_TEST_ACCOUNT: Arc<EntrySealedCommitted> =
Arc::new(TEST_ACCOUNT.clone().into_sealed_committed());
}
#[test]
fn test_pre_create_deny() {
// Test creating with class: system is rejected.
let e = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(
Attribute::DisplayName,
Value::Utf8("testperson".to_string())
)
);
let create = vec![e];
let preload = PRELOAD.clone();
run_create_test!(
Err(OperationError::SystemProtectedObject),
preload,
create,
Some(E_TEST_ACCOUNT.clone()),
|_| {}
);
}
#[test]
fn test_pre_modify_system_deny() {
// Test modify of class to a system is denied
let e = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(
Attribute::DisplayName,
Value::Utf8("testperson".to_string())
)
);
let mut preload = PRELOAD.clone();
preload.push(e);
run_modify_test!(
Err(OperationError::SystemProtectedObject),
preload,
filter!(f_eq(Attribute::Name, PartialValue::new_iname("testperson"))),
modlist!([
m_purge(Attribute::DisplayName),
m_pres(Attribute::DisplayName, &Value::new_utf8s("system test")),
]),
Some(E_TEST_ACCOUNT.clone()),
|_| {},
|_| {}
);
}
#[test]
fn test_pre_modify_class_add_deny() {
// Show that adding a system class is denied
// TODO: replace this with a `SchemaClass` object
let e = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::ClassType.to_value()),
(Attribute::ClassName, Value::new_iutf8("testclass")),
(
Attribute::Uuid,
Value::Uuid(uuid::uuid!("66c68b2f-d02c-4243-8013-7946e40fe321"))
),
(
Attribute::Description,
Value::Utf8("class test".to_string())
)
);
let mut preload = PRELOAD.clone();
preload.push(e);
run_modify_test!(
Ok(()),
preload,
filter!(f_eq(
Attribute::ClassName,
PartialValue::new_iutf8("testclass")
)),
modlist!([
m_pres(Attribute::May, &Value::from(Attribute::Name)),
m_pres(Attribute::Must, &Value::from(Attribute::Name)),
]),
Some(E_TEST_ACCOUNT.clone()),
|_| {},
|_| {}
);
}
#[test]
fn test_pre_delete_deny() {
// Test deleting with class: system is rejected.
let e = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(
Attribute::DisplayName,
Value::Utf8("testperson".to_string())
)
);
let mut preload = PRELOAD.clone();
preload.push(e);
run_delete_test!(
Err(OperationError::SystemProtectedObject),
preload,
filter!(f_eq(Attribute::Name, PartialValue::new_iname("testperson"))),
Some(E_TEST_ACCOUNT.clone()),
|_| {}
);
}
#[test]
fn test_modify_domain() {
// Can edit *my* domain_ssid and domain_name
// Show that adding a system class is denied
let e = entry_init!(
(Attribute::Class, EntryClass::DomainInfo.to_value()),
(Attribute::Name, Value::new_iname("domain_example.net.au")),
(Attribute::Uuid, Value::Uuid(uuid::uuid!("96fd1112-28bc-48ae-9dda-5acb4719aaba"))),
(
Attribute::Description,
Value::new_utf8s("Demonstration of a remote domain's info being created for uuid generation in test_modify_domain")
),
(Attribute::DomainUuid, Value::Uuid(uuid::uuid!("96fd1112-28bc-48ae-9dda-5acb4719aaba"))),
(Attribute::DomainName, Value::new_iname("example.net.au")),
(Attribute::DomainDisplayName, Value::Utf8("example.net.au".to_string())),
(Attribute::DomainSsid, Value::Utf8("Example_Wifi".to_string())),
(Attribute::Version, Value::Uint32(1))
);
let mut preload = PRELOAD.clone();
preload.push(e);
run_modify_test!(
Ok(()),
preload,
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("domain_example.net.au")
)),
modlist!([
m_purge(Attribute::DomainSsid),
m_pres(Attribute::DomainSsid, &Value::new_utf8s("NewExampleWifi")),
]),
Some(E_TEST_ACCOUNT.clone()),
|_| {},
|_| {}
);
}
#[test]
fn test_ext_create_domain() {
// can not add a domain_info type - note the lack of class: system
let e = entry_init!(
(Attribute::Class, EntryClass::DomainInfo.to_value()),
(Attribute::Name, Value::new_iname("domain_example.net.au")),
(Attribute::Uuid, Value::Uuid(uuid::uuid!("96fd1112-28bc-48ae-9dda-5acb4719aaba"))),
(
Attribute::Description,
Value::new_utf8s("Demonstration of a remote domain's info being created for uuid generation in test_modify_domain")
),
(Attribute::DomainUuid, Value::Uuid(uuid::uuid!("96fd1112-28bc-48ae-9dda-5acb4719aaba"))),
(Attribute::DomainName, Value::new_iname("example.net.au")),
(Attribute::DomainDisplayName, Value::Utf8("example.net.au".to_string())),
(Attribute::DomainSsid, Value::Utf8("Example_Wifi".to_string())),
(Attribute::Version, Value::Uint32(1))
);
let create = vec![e];
let preload = PRELOAD.clone();
run_create_test!(
Err(OperationError::SystemProtectedObject),
preload,
create,
Some(E_TEST_ACCOUNT.clone()),
|_| {}
);
}
#[test]
fn test_delete_domain() {
// On the real thing we have a class: system, but to prove the point ...
let e = entry_init!(
(Attribute::Class, EntryClass::DomainInfo.to_value()),
(Attribute::Name, Value::new_iname("domain_example.net.au")),
(Attribute::Uuid, Value::Uuid(uuid::uuid!("96fd1112-28bc-48ae-9dda-5acb4719aaba"))),
(
Attribute::Description,
Value::new_utf8s("Demonstration of a remote domain's info being created for uuid generation in test_modify_domain")
),
(Attribute::DomainUuid, Value::Uuid(uuid::uuid!("96fd1112-28bc-48ae-9dda-5acb4719aaba"))),
(Attribute::DomainName, Value::new_iname("example.net.au")),
(Attribute::DomainDisplayName, Value::Utf8("example.net.au".to_string())),
(Attribute::DomainSsid, Value::Utf8("Example_Wifi".to_string())),
(Attribute::Version, Value::Uint32(1))
);
let mut preload = PRELOAD.clone();
preload.push(e);
run_delete_test!(
Err(OperationError::SystemProtectedObject),
preload,
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("domain_example.net.au")
)),
Some(E_TEST_ACCOUNT.clone()),
|_| {}
);
}
}

View file

@ -1,7 +1,7 @@
use super::proto::*;
use crate::plugins::Plugins;
use crate::prelude::*;
use crate::server::{ChangeFlag, ServerPhase};
use crate::server::ChangeFlag;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
@ -343,7 +343,7 @@ impl QueryServerWriteTransaction<'_> {
}
}
#[instrument(level = "info", skip_all)]
#[instrument(level = "debug", skip_all)]
fn consumer_apply_changes_v1(
&mut self,
ctx_domain_version: DomainVersion,
@ -548,7 +548,7 @@ impl QueryServerWriteTransaction<'_> {
Ok(())
}
#[instrument(level = "info", skip_all)]
#[instrument(level = "debug", skip_all)]
fn consumer_apply_refresh_v1(
&mut self,
ctx_domain_version: DomainVersion,
@ -583,7 +583,6 @@ impl QueryServerWriteTransaction<'_> {
};
// == ⚠️ Below this point we begin to make changes! ==
self.set_phase_bootstrap();
// Update the d_uuid. This is what defines us as being part of this repl topology!
self.be_txn
@ -598,6 +597,7 @@ impl QueryServerWriteTransaction<'_> {
self.reset_server_uuid()?;
// Delete all entries - *proper delete, not just tombstone!*
self.be_txn
.danger_delete_all_db_content()
.inspect_err(|err| {
@ -609,12 +609,6 @@ impl QueryServerWriteTransaction<'_> {
error!(?err, "Failed to reset in memory schema to clean state");
})?;
// Reindex now to force some basic indexes to exist as we consume the schema
// from our replica.
self.reindex(false).inspect_err(|err| {
error!(?err, "Failed to reload schema");
})?;
// Apply the schema entries first. This is the foundation that everything
// else will build upon!
self.consumer_refresh_create_entries(ctx_schema_entries)
@ -627,9 +621,6 @@ impl QueryServerWriteTransaction<'_> {
error!(?err, "Failed to reload schema");
})?;
// Schema is now ready
self.set_phase(ServerPhase::SchemaReady);
// We have to reindex to force all the existing indexes to be dumped
// and recreated before we start to import.
self.reindex(false).inspect_err(|err| {
@ -661,10 +652,7 @@ impl QueryServerWriteTransaction<'_> {
| ChangeFlag::KEY_MATERIAL,
);
// Domain info is now ready.
self.set_phase(ServerPhase::DomainInfoReady);
// ==== That's it! We are GOOD to go! ====
// That's it! We are GOOD to go!
// Create all the entries. Note we don't hit plugins here beside post repl plugs.
self.consumer_refresh_create_entries(ctx_entries)
@ -684,9 +672,6 @@ impl QueryServerWriteTransaction<'_> {
error!(?err, "Unable to update RUV with supplier ranges.");
})?;
// Refresh complete
self.set_phase(ServerPhase::Running);
Ok(())
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,16 @@
use super::profiles::{
AccessControlCreateResolved, AccessControlReceiverCondition, AccessControlTargetCondition,
};
use super::protected::PROTECTED_ENTRY_CLASSES;
use crate::prelude::*;
use std::collections::BTreeSet;
pub(super) enum CreateResult {
Deny,
Denied,
Grant,
}
enum IResult {
Deny,
Denied,
Grant,
Ignore,
}
@ -26,25 +25,25 @@ pub(super) fn apply_create_access<'a>(
// This module can never yield a grant.
match protected_filter_entry(ident, entry) {
IResult::Deny => denied = true,
IResult::Denied => denied = true,
IResult::Grant | IResult::Ignore => {}
}
match create_filter_entry(ident, related_acp, entry) {
IResult::Deny => denied = true,
IResult::Denied => denied = true,
IResult::Grant => grant = true,
IResult::Ignore => {}
}
if denied {
// Something explicitly said no.
CreateResult::Deny
CreateResult::Denied
} else if grant {
// Something said yes
CreateResult::Grant
} else {
// Nothing said yes.
CreateResult::Deny
CreateResult::Denied
}
}
@ -61,7 +60,7 @@ fn create_filter_entry<'a>(
}
IdentType::Synch(_) => {
security_critical!("Blocking sync check");
return IResult::Deny;
return IResult::Denied;
}
IdentType::User(_) => {}
};
@ -70,7 +69,7 @@ fn create_filter_entry<'a>(
match ident.access_scope() {
AccessScope::ReadOnly | AccessScope::Synchronise => {
security_access!("denied ❌ - identity access scope is not permitted to create");
return IResult::Deny;
return IResult::Denied;
}
AccessScope::ReadWrite => {
// As you were
@ -97,7 +96,7 @@ fn create_filter_entry<'a>(
Some(s) => s.collect(),
None => {
admin_error!("Class set failed to build - corrupted entry?");
return IResult::Deny;
return IResult::Denied;
}
};
@ -174,22 +173,22 @@ fn protected_filter_entry(ident: &Identity, entry: &Entry<EntryInit, EntryNew>)
}
IdentType::Synch(_) => {
security_access!("sync agreements may not directly create entities");
IResult::Deny
IResult::Denied
}
IdentType::User(_) => {
// Now check things ...
if let Some(classes) = entry.get_ava_as_iutf8(Attribute::Class) {
if classes.is_disjoint(&PROTECTED_ENTRY_CLASSES) {
// It's different, go ahead
IResult::Ignore
} else {
// Block the mod, something is present
// For now we just block create on sync object
if let Some(classes) = entry.get_ava_set(Attribute::Class) {
if classes.contains(&EntryClass::SyncObject.into()) {
// Block the mod
security_access!("attempt to create with protected class type");
IResult::Deny
IResult::Denied
} else {
IResult::Ignore
}
} else {
// Nothing to check - this entry will fail to create anyway because it has
// no classes
// Nothing to check.
IResult::Ignore
}
}

View file

@ -1,17 +1,16 @@
use super::profiles::{
AccessControlDeleteResolved, AccessControlReceiverCondition, AccessControlTargetCondition,
};
use super::protected::PROTECTED_ENTRY_CLASSES;
use crate::prelude::*;
use std::sync::Arc;
pub(super) enum DeleteResult {
Deny,
Denied,
Grant,
}
enum IResult {
Deny,
Denied,
Grant,
Ignore,
}
@ -25,25 +24,25 @@ pub(super) fn apply_delete_access<'a>(
let mut grant = false;
match protected_filter_entry(ident, entry) {
IResult::Deny => denied = true,
IResult::Denied => denied = true,
IResult::Grant | IResult::Ignore => {}
}
match delete_filter_entry(ident, related_acp, entry) {
IResult::Deny => denied = true,
IResult::Denied => denied = true,
IResult::Grant => grant = true,
IResult::Ignore => {}
}
if denied {
// Something explicitly said no.
DeleteResult::Deny
DeleteResult::Denied
} else if grant {
// Something said yes
DeleteResult::Grant
} else {
// Nothing said yes.
DeleteResult::Deny
DeleteResult::Denied
}
}
@ -60,7 +59,7 @@ fn delete_filter_entry<'a>(
}
IdentType::Synch(_) => {
security_critical!("Blocking sync check");
return IResult::Deny;
return IResult::Denied;
}
IdentType::User(_) => {}
};
@ -69,7 +68,7 @@ fn delete_filter_entry<'a>(
match ident.access_scope() {
AccessScope::ReadOnly | AccessScope::Synchronise => {
security_access!("denied ❌ - identity access scope is not permitted to delete");
return IResult::Deny;
return IResult::Denied;
}
AccessScope::ReadWrite => {
// As you were
@ -153,30 +152,28 @@ fn protected_filter_entry(ident: &Identity, entry: &Arc<EntrySealedCommitted>) -
}
IdentType::Synch(_) => {
security_access!("sync agreements may not directly delete entities");
IResult::Deny
IResult::Denied
}
IdentType::User(_) => {
// Now check things ...
// For now we just block create on sync object
if let Some(classes) = entry.get_ava_set(Attribute::Class) {
if classes.contains(&EntryClass::SyncObject.into()) {
// Block the mod
security_access!("attempt to delete with protected class type");
return IResult::Denied;
}
};
// Prevent deletion of entries that exist in the system controlled entry range.
if entry.get_uuid() <= UUID_ANONYMOUS {
security_access!("attempt to delete system builtin entry");
return IResult::Deny;
return IResult::Denied;
}
// Prevent deleting some protected types.
if let Some(classes) = entry.get_ava_as_iutf8(Attribute::Class) {
if classes.is_disjoint(&PROTECTED_ENTRY_CLASSES) {
// It's different, go ahead
IResult::Ignore
} else {
// Block the mod, something is present
security_access!("attempt to create with protected class type");
IResult::Deny
}
} else {
// Nothing to check - this entry will fail to create anyway because it has
// no classes
IResult::Ignore
}
// Checks exhausted, no more input from us
IResult::Ignore
}
}
}

View file

@ -50,13 +50,12 @@ mod create;
mod delete;
mod modify;
pub mod profiles;
mod protected;
mod search;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Access {
Grant,
Deny,
Denied,
Allow(BTreeSet<Attribute>),
}
@ -64,7 +63,7 @@ impl From<&Access> for ScimAttributeEffectiveAccess {
fn from(value: &Access) -> Self {
match value {
Access::Grant => Self::Grant,
Access::Deny => Self::Deny,
Access::Denied => Self::Denied,
Access::Allow(set) => Self::Allow(set.clone()),
}
}
@ -73,7 +72,7 @@ impl From<&Access> for ScimAttributeEffectiveAccess {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AccessClass {
Grant,
Deny,
Denied,
Allow(BTreeSet<AttrString>),
}
@ -87,22 +86,12 @@ pub struct AccessEffectivePermission {
pub search: Access,
pub modify_pres: Access,
pub modify_rem: Access,
pub modify_pres_class: AccessClass,
pub modify_rem_class: AccessClass,
pub modify_class: AccessClass,
}
pub enum AccessBasicResult {
pub enum AccessResult {
// Deny this operation unconditionally.
Deny,
// Unbounded allow, provided no deny state exists.
Grant,
// This module makes no decisions about this entry.
Ignore,
}
pub enum AccessSrchResult {
// Deny this operation unconditionally.
Deny,
Denied,
// Unbounded allow, provided no deny state exists.
Grant,
// This module makes no decisions about this entry.
@ -110,37 +99,24 @@ pub enum AccessSrchResult {
// Limit the allowed attr set to this - this doesn't
// allow anything, it constrains what might be allowed
// by a later module.
/*
Constrain {
attr: BTreeSet<Attribute>,
},
*/
Allow { attr: BTreeSet<Attribute> },
Constrain(BTreeSet<Attribute>),
// Allow these attributes within constraints.
Allow(BTreeSet<Attribute>),
}
pub enum AccessModResult<'a> {
#[allow(dead_code)]
pub enum AccessResultClass<'a> {
// Deny this operation unconditionally.
Deny,
// Unbounded allow, provided no deny state exists.
// Grant,
Denied,
// Unbounded allow, provided no denied exists.
Grant,
// This module makes no decisions about this entry.
Ignore,
// Limit the allowed attr set to this - this doesn't
// allow anything, it constrains what might be allowed
// by a later module.
Constrain {
pres_attr: BTreeSet<Attribute>,
rem_attr: BTreeSet<Attribute>,
pres_cls: Option<BTreeSet<&'a str>>,
rem_cls: Option<BTreeSet<&'a str>>,
},
// Allow these modifications within constraints.
Allow {
pres_attr: BTreeSet<Attribute>,
rem_attr: BTreeSet<Attribute>,
pres_class: BTreeSet<&'a str>,
rem_class: BTreeSet<&'a str>,
},
// allow anything, it constrains what might be allowed.
Constrain(BTreeSet<&'a str>),
// Allow these attributes within constraints.
Allow(BTreeSet<&'a str>),
}
// =========================================================================
@ -327,7 +303,7 @@ pub trait AccessControlsTransaction<'a> {
.into_iter()
.filter(|e| {
match apply_search_access(ident, related_acp.as_slice(), e) {
SearchResult::Deny => false,
SearchResult::Denied => false,
SearchResult::Grant => true,
SearchResult::Allow(allowed_attrs) => {
// The allow set constrained.
@ -425,7 +401,7 @@ pub trait AccessControlsTransaction<'a> {
.into_iter()
.filter_map(|entry| {
match apply_search_access(&se.ident, &search_related_acp, &entry) {
SearchResult::Deny => {
SearchResult::Denied => {
None
}
SearchResult::Grant => {
@ -560,8 +536,7 @@ pub trait AccessControlsTransaction<'a> {
// Build the set of classes that we to work on, only in terms of "addition". To remove
// I think we have no limit, but ... william of the future may find a problem with this
// policy.
let mut requested_pres_classes: BTreeSet<&str> = Default::default();
let mut requested_rem_classes: BTreeSet<&str> = Default::default();
let mut requested_classes: BTreeSet<&str> = Default::default();
for modify in me.modlist.iter() {
match modify {
@ -573,33 +548,27 @@ pub trait AccessControlsTransaction<'a> {
// existence, and second, we would have failed the mod at schema checking
// earlier in the process as these were not correctly type. As a result
// we can trust these to be correct here and not to be "None".
requested_pres_classes.extend(v.to_str())
requested_classes.extend(v.to_str())
}
}
Modify::Removed(a, v) => {
if a == Attribute::Class.as_ref() {
requested_rem_classes.extend(v.to_str())
requested_classes.extend(v.to_str())
}
}
Modify::Set(a, v) => {
if a == Attribute::Class.as_ref() {
// This is a reasonably complex case - we actually have to contemplate
// the difference between what exists and what doesn't, but that's per-entry.
//
// for now, we treat this as both pres and rem, but I think that ultimately
// to fix this we need to make all modifies apply in terms of "batch mod"
requested_pres_classes.extend(v.as_iutf8_iter().into_iter().flatten());
requested_rem_classes.extend(v.as_iutf8_iter().into_iter().flatten());
// flatten to remove the option down to an iterator
requested_classes.extend(v.as_iutf8_iter().into_iter().flatten())
}
}
_ => {}
}
}
debug!(?requested_pres, "Requested present attribute set");
debug!(?requested_rem, "Requested remove attribute set");
debug!(?requested_pres_classes, "Requested present class set");
debug!(?requested_rem_classes, "Requested remove class set");
debug!(?requested_pres, "Requested present set");
debug!(?requested_rem, "Requested remove set");
debug!(?requested_classes, "Requested class set");
let sync_agmts = self.get_sync_agreements();
@ -607,16 +576,9 @@ pub trait AccessControlsTransaction<'a> {
debug!(entry_id = %e.get_display_id());
match apply_modify_access(&me.ident, related_acp.as_slice(), sync_agmts, e) {
ModifyResult::Deny => false,
ModifyResult::Denied => false,
ModifyResult::Grant => true,
ModifyResult::Allow {
pres,
rem,
pres_cls,
rem_cls,
} => {
let mut decision = true;
ModifyResult::Allow { pres, rem, cls } => {
if !requested_pres.is_subset(&pres) {
security_error!("requested_pres is not a subset of allowed");
security_error!(
@ -624,41 +586,23 @@ pub trait AccessControlsTransaction<'a> {
requested_pres,
pres
);
decision = false
};
if !requested_rem.is_subset(&rem) {
false
} else if !requested_rem.is_subset(&rem) {
security_error!("requested_rem is not a subset of allowed");
security_error!("requested_rem: {:?} !⊆ allowed: {:?}", requested_rem, rem);
decision = false;
};
if !requested_pres_classes.is_subset(&pres_cls) {
security_error!("requested_pres_classes is not a subset of allowed");
false
} else if !requested_classes.is_subset(&cls) {
security_error!("requested_classes is not a subset of allowed");
security_error!(
"requested_pres_classes: {:?} !⊆ allowed: {:?}",
requested_pres_classes,
pres_cls
"requested_classes: {:?} !⊆ allowed: {:?}",
requested_classes,
cls
);
decision = false;
};
if !requested_rem_classes.is_subset(&rem_cls) {
security_error!("requested_rem_classes is not a subset of allowed");
security_error!(
"requested_rem_classes: {:?} !⊆ allowed: {:?}",
requested_rem_classes,
rem_cls
);
decision = false;
}
if decision {
false
} else {
debug!("passed pres, rem, classes check.");
}
// Yield the result
decision
true
} // if acc == false
}
}
});
@ -724,55 +668,47 @@ pub trait AccessControlsTransaction<'a> {
})
.collect();
let mut requested_pres_classes: BTreeSet<&str> = Default::default();
let mut requested_rem_classes: BTreeSet<&str> = Default::default();
for modify in modlist.iter() {
match modify {
// Build the set of classes that we to work on, only in terms of "addition". To remove
// I think we have no limit, but ... william of the future may find a problem with this
// policy.
let requested_classes: BTreeSet<&str> = modlist
.iter()
.filter_map(|m| match m {
Modify::Present(a, v) => {
if a == Attribute::Class.as_ref() {
requested_pres_classes.extend(v.to_str())
// Here we have an option<&str> which could mean there is a risk of
// a malicious entity attempting to trick us by masking class mods
// in non-iutf8 types. However, the server first won't respect their
// existence, and second, we would have failed the mod at schema checking
// earlier in the process as these were not correctly type. As a result
// we can trust these to be correct here and not to be "None".
v.to_str()
} else {
None
}
}
Modify::Removed(a, v) => {
if a == Attribute::Class.as_ref() {
requested_rem_classes.extend(v.to_str())
v.to_str()
} else {
None
}
}
Modify::Set(a, v) => {
if a == Attribute::Class.as_ref() {
// This is a reasonably complex case - we actually have to contemplate
// the difference between what exists and what doesn't, but that's per-entry.
//
// for now, we treat this as both pres and rem, but I think that ultimately
// to fix this we need to make all modifies apply in terms of "batch mod"
requested_pres_classes.extend(v.as_iutf8_iter().into_iter().flatten());
requested_rem_classes.extend(v.as_iutf8_iter().into_iter().flatten());
}
}
_ => {}
}
}
_ => None,
})
.collect();
debug!(?requested_pres, "Requested present set");
debug!(?requested_rem, "Requested remove set");
debug!(?requested_pres_classes, "Requested present class set");
debug!(?requested_rem_classes, "Requested remove class set");
debug!(?requested_classes, "Requested class set");
debug!(entry_id = %e.get_display_id());
let sync_agmts = self.get_sync_agreements();
match apply_modify_access(&me.ident, related_acp.as_slice(), sync_agmts, e) {
ModifyResult::Deny => false,
ModifyResult::Denied => false,
ModifyResult::Grant => true,
ModifyResult::Allow {
pres,
rem,
pres_cls,
rem_cls,
} => {
let mut decision = true;
ModifyResult::Allow { pres, rem, cls } => {
if !requested_pres.is_subset(&pres) {
security_error!("requested_pres is not a subset of allowed");
security_error!(
@ -780,41 +716,23 @@ pub trait AccessControlsTransaction<'a> {
requested_pres,
pres
);
decision = false
};
if !requested_rem.is_subset(&rem) {
false
} else if !requested_rem.is_subset(&rem) {
security_error!("requested_rem is not a subset of allowed");
security_error!("requested_rem: {:?} !⊆ allowed: {:?}", requested_rem, rem);
decision = false;
};
if !requested_pres_classes.is_subset(&pres_cls) {
security_error!("requested_pres_classes is not a subset of allowed");
false
} else if !requested_classes.is_subset(&cls) {
security_error!("requested_classes is not a subset of allowed");
security_error!(
"requested_classes: {:?} !⊆ allowed: {:?}",
requested_pres_classes,
pres_cls
requested_classes,
cls
);
decision = false;
};
if !requested_rem_classes.is_subset(&rem_cls) {
security_error!("requested_rem_classes is not a subset of allowed");
security_error!(
"requested_classes: {:?} !⊆ allowed: {:?}",
requested_rem_classes,
rem_cls
);
decision = false;
}
if decision {
debug!("passed pres, rem, classes check.");
}
// Yield the result
decision
false
} else {
security_access!("passed pres, rem, classes check.");
true
} // if acc == false
}
}
});
@ -862,7 +780,7 @@ pub trait AccessControlsTransaction<'a> {
// For each entry
let r = entries.iter().all(|e| {
match apply_create_access(&ce.ident, related_acp.as_slice(), e) {
CreateResult::Deny => false,
CreateResult::Denied => false,
CreateResult::Grant => true,
}
});
@ -918,7 +836,7 @@ pub trait AccessControlsTransaction<'a> {
// For each entry
let r = entries.iter().all(|e| {
match apply_delete_access(&de.ident, related_acp.as_slice(), e) {
DeleteResult::Deny => false,
DeleteResult::Denied => false,
DeleteResult::Grant => true,
}
});
@ -1007,7 +925,7 @@ pub trait AccessControlsTransaction<'a> {
) -> AccessEffectivePermission {
// == search ==
let search_effective = match apply_search_access(ident, search_related_acp, entry) {
SearchResult::Deny => Access::Deny,
SearchResult::Denied => Access::Denied,
SearchResult::Grant => Access::Grant,
SearchResult::Allow(allowed_attrs) => {
// Bound by requested attrs?
@ -1016,30 +934,14 @@ pub trait AccessControlsTransaction<'a> {
};
// == modify ==
let (modify_pres, modify_rem, modify_pres_class, modify_rem_class) =
let (modify_pres, modify_rem, modify_class) =
match apply_modify_access(ident, modify_related_acp, sync_agmts, entry) {
ModifyResult::Deny => (
Access::Deny,
Access::Deny,
AccessClass::Deny,
AccessClass::Deny,
),
ModifyResult::Grant => (
Access::Grant,
Access::Grant,
AccessClass::Grant,
AccessClass::Grant,
),
ModifyResult::Allow {
pres,
rem,
pres_cls,
rem_cls,
} => (
ModifyResult::Denied => (Access::Denied, Access::Denied, AccessClass::Denied),
ModifyResult::Grant => (Access::Grant, Access::Grant, AccessClass::Grant),
ModifyResult::Allow { pres, rem, cls } => (
Access::Allow(pres.into_iter().collect()),
Access::Allow(rem.into_iter().collect()),
AccessClass::Allow(pres_cls.into_iter().map(|s| s.into()).collect()),
AccessClass::Allow(rem_cls.into_iter().map(|s| s.into()).collect()),
AccessClass::Allow(cls.into_iter().map(|s| s.into()).collect()),
),
};
@ -1047,7 +949,7 @@ pub trait AccessControlsTransaction<'a> {
let delete_status = apply_delete_access(ident, delete_related_acp, entry);
let delete = match delete_status {
DeleteResult::Deny => false,
DeleteResult::Denied => false,
DeleteResult::Grant => true,
};
@ -1058,8 +960,7 @@ pub trait AccessControlsTransaction<'a> {
search: search_effective,
modify_pres,
modify_rem,
modify_pres_class,
modify_rem_class,
modify_class,
}
}
}
@ -2265,8 +2166,6 @@ mod tests {
"name class",
// And the class allowed is account
EntryClass::Account.into(),
// And the class allowed is account
EntryClass::Account.into(),
);
// Allow member, class is group. IE not account
let acp_deny = AccessControlModify::from_raw(
@ -2283,8 +2182,8 @@ mod tests {
"member class",
// Allow rem name and class
"member class",
EntryClass::Group.into(),
EntryClass::Group.into(),
// And the class allowed is account
"group",
);
// Does not have a pres or rem class in attrs
let acp_no_class = AccessControlModify::from_raw(
@ -2302,8 +2201,7 @@ mod tests {
// Allow rem name and class
"name class",
// And the class allowed is NOT an account ...
EntryClass::Group.into(),
EntryClass::Group.into(),
"group",
);
// Test allowed pres
@ -2389,7 +2287,6 @@ mod tests {
"name class",
// And the class allowed is account
EntryClass::Account.into(),
EntryClass::Account.into(),
);
test_acp_modify!(&me_pres_ro, vec![acp_allow.clone()], &r_set, false);
@ -2717,8 +2614,7 @@ mod tests {
search: Access::Allow(btreeset![Attribute::Name]),
modify_pres: Access::Allow(BTreeSet::new()),
modify_rem: Access::Allow(BTreeSet::new()),
modify_pres_class: AccessClass::Allow(BTreeSet::new()),
modify_rem_class: AccessClass::Allow(BTreeSet::new()),
modify_class: AccessClass::Allow(BTreeSet::new()),
}]
)
}
@ -2751,7 +2647,6 @@ mod tests {
Attribute::Name.as_ref(),
Attribute::Name.as_ref(),
EntryClass::Object.into(),
EntryClass::Object.into(),
)],
&r_set,
vec![AccessEffectivePermission {
@ -2761,8 +2656,7 @@ mod tests {
search: Access::Allow(BTreeSet::new()),
modify_pres: Access::Allow(btreeset![Attribute::Name]),
modify_rem: Access::Allow(btreeset![Attribute::Name]),
modify_pres_class: AccessClass::Allow(btreeset![EntryClass::Object.into()]),
modify_rem_class: AccessClass::Allow(btreeset![EntryClass::Object.into()]),
modify_class: AccessClass::Allow(btreeset![EntryClass::Object.into()]),
}]
)
}
@ -2902,7 +2796,6 @@ mod tests {
&format!("{} {}", Attribute::UserAuthTokenSession, Attribute::Name),
// And the class allowed is account, we don't use it though.
EntryClass::Account.into(),
EntryClass::Account.into(),
);
// NOTE! Syntax doesn't matter here, we just need to assert if the attr exists
@ -3403,7 +3296,6 @@ mod tests {
"name class",
// And the class allowed is account
EntryClass::Account.into(),
EntryClass::Account.into(),
);
// Test allowed pres
@ -3532,185 +3424,4 @@ mod tests {
// Finally test it!
test_acp_search_reduce!(&se_anon_ro, vec![acp], r_set, ex_anon_some);
}
#[test]
fn test_access_protected_deny_create() {
sketching::test_init();
let ev1 = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(UUID_TEST_ACCOUNT_1))
);
let r1_set = vec![ev1];
let ev2 = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(UUID_TEST_ACCOUNT_1))
);
let r2_set = vec![ev2];
let ce_admin = CreateEvent::new_impersonate_identity(
Identity::from_impersonate_entry_readwrite(E_TEST_ACCOUNT_1.clone()),
vec![],
);
let acp = AccessControlCreate::from_raw(
"test_create",
Uuid::new_v4(),
// Apply to admin
UUID_TEST_GROUP_1,
// To create matching filter testperson
// Can this be empty?
filter_valid!(f_eq(
Attribute::Name,
PartialValue::new_iname("testperson1")
)),
// classes
EntryClass::Account.into(),
// attrs
"class name uuid",
);
// Test allowed to create
test_acp_create!(&ce_admin, vec![acp.clone()], &r1_set, true);
// Test reject create (not allowed attr)
test_acp_create!(&ce_admin, vec![acp.clone()], &r2_set, false);
}
#[test]
fn test_access_protected_deny_delete() {
sketching::test_init();
let ev1 = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(UUID_TEST_ACCOUNT_1))
)
.into_sealed_committed();
let r1_set = vec![Arc::new(ev1)];
let ev2 = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(UUID_TEST_ACCOUNT_1))
)
.into_sealed_committed();
let r2_set = vec![Arc::new(ev2)];
let de = DeleteEvent::new_impersonate_entry(
E_TEST_ACCOUNT_1.clone(),
filter_all!(f_eq(
Attribute::Name,
PartialValue::new_iname("testperson1")
)),
);
let acp = AccessControlDelete::from_raw(
"test_delete",
Uuid::new_v4(),
// Apply to admin
UUID_TEST_GROUP_1,
// To delete testperson
filter_valid!(f_eq(
Attribute::Name,
PartialValue::new_iname("testperson1")
)),
);
// Test allowed to delete
test_acp_delete!(&de, vec![acp.clone()], &r1_set, true);
// Test not allowed to delete
test_acp_delete!(&de, vec![acp.clone()], &r2_set, false);
}
#[test]
fn test_access_protected_deny_modify() {
sketching::test_init();
let ev1 = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(UUID_TEST_ACCOUNT_1))
)
.into_sealed_committed();
let r1_set = vec![Arc::new(ev1)];
let ev2 = entry_init!(
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(UUID_TEST_ACCOUNT_1))
)
.into_sealed_committed();
let r2_set = vec![Arc::new(ev2)];
// Allow name and class, class is account
let acp_allow = AccessControlModify::from_raw(
"test_modify_allow",
Uuid::new_v4(),
// Apply to admin
UUID_TEST_GROUP_1,
// To modify testperson
filter_valid!(f_eq(
Attribute::Name,
PartialValue::new_iname("testperson1")
)),
// Allow pres disp name and class
"displayname class",
// Allow rem disp name and class
"displayname class",
// And the classes allowed to add/rem are as such
"system recycled",
"system recycled",
);
let me_pres = ModifyEvent::new_impersonate_entry(
E_TEST_ACCOUNT_1.clone(),
filter_all!(f_eq(
Attribute::Name,
PartialValue::new_iname("testperson1")
)),
modlist!([m_pres(Attribute::DisplayName, &Value::new_utf8s("value"))]),
);
// Test allowed pres
test_acp_modify!(&me_pres, vec![acp_allow.clone()], &r1_set, true);
// Test not allowed pres (due to system class)
test_acp_modify!(&me_pres, vec![acp_allow.clone()], &r2_set, false);
// Test that we can not remove class::system
let me_rem_sys = ModifyEvent::new_impersonate_entry(
E_TEST_ACCOUNT_1.clone(),
filter_all!(f_eq(
Attribute::Class,
PartialValue::new_iname("testperson1")
)),
modlist!([m_remove(
Attribute::Class,
&EntryClass::System.to_partialvalue()
)]),
);
test_acp_modify!(&me_rem_sys, vec![acp_allow.clone()], &r2_set, false);
// Ensure that we can't add recycled.
let me_pres = ModifyEvent::new_impersonate_entry(
E_TEST_ACCOUNT_1.clone(),
filter_all!(f_eq(
Attribute::Name,
PartialValue::new_iname("testperson1")
)),
modlist!([m_pres(Attribute::Class, &EntryClass::Recycled.to_value())]),
);
test_acp_modify!(&me_pres, vec![acp_allow.clone()], &r1_set, false);
}
}

View file

@ -1,25 +1,21 @@
use crate::prelude::*;
use hashbrown::HashMap;
use std::collections::BTreeSet;
use super::profiles::{
AccessControlModify, AccessControlModifyResolved, AccessControlReceiverCondition,
AccessControlTargetCondition,
};
use super::protected::{
LOCKED_ENTRY_CLASSES, PROTECTED_MOD_ENTRY_CLASSES, PROTECTED_MOD_PRES_ENTRY_CLASSES,
PROTECTED_MOD_REM_ENTRY_CLASSES,
};
use super::{AccessBasicResult, AccessModResult};
use crate::prelude::*;
use hashbrown::HashMap;
use std::collections::BTreeSet;
use super::{AccessResult, AccessResultClass};
use std::sync::Arc;
pub(super) enum ModifyResult<'a> {
Deny,
Denied,
Grant,
Allow {
pres: BTreeSet<Attribute>,
rem: BTreeSet<Attribute>,
pres_cls: BTreeSet<&'a str>,
rem_cls: BTreeSet<&'a str>,
cls: BTreeSet<&'a str>,
},
}
@ -31,17 +27,12 @@ pub(super) fn apply_modify_access<'a>(
) -> ModifyResult<'a> {
let mut denied = false;
let mut grant = false;
let mut constrain_pres = BTreeSet::default();
let mut allow_pres = BTreeSet::default();
let mut constrain_rem = BTreeSet::default();
let mut allow_rem = BTreeSet::default();
let mut constrain_pres_cls = BTreeSet::default();
let mut allow_pres_cls = BTreeSet::default();
let mut constrain_rem_cls = BTreeSet::default();
let mut allow_rem_cls = BTreeSet::default();
let mut constrain_cls = BTreeSet::default();
let mut allow_cls = BTreeSet::default();
// Some useful references.
// - needed for checking entry manager conditions.
@ -52,53 +43,28 @@ pub(super) fn apply_modify_access<'a>(
// kind of being three operations all in one.
match modify_ident_test(ident) {
AccessBasicResult::Deny => denied = true,
AccessBasicResult::Grant => grant = true,
AccessBasicResult::Ignore => {}
}
// Check with protected if we should proceed.
match modify_protected_attrs(ident, entry) {
AccessModResult::Deny => denied = true,
AccessModResult::Constrain {
mut pres_attr,
mut rem_attr,
pres_cls,
rem_cls,
} => {
constrain_rem.append(&mut rem_attr);
constrain_pres.append(&mut pres_attr);
if let Some(mut pres_cls) = pres_cls {
constrain_pres_cls.append(&mut pres_cls);
}
if let Some(mut rem_cls) = rem_cls {
constrain_rem_cls.append(&mut rem_cls);
}
}
// Can't grant.
// AccessModResult::Grant |
// Can't allow
AccessModResult::Allow { .. } | AccessModResult::Ignore => {}
AccessResult::Denied => denied = true,
AccessResult::Grant => grant = true,
AccessResult::Ignore => {}
AccessResult::Constrain(mut set) => constrain_pres.append(&mut set),
AccessResult::Allow(mut set) => allow_pres.append(&mut set),
}
if !grant && !denied {
// Check with protected if we should proceed.
// If it's a sync entry, constrain it.
match modify_sync_constrain(ident, entry, sync_agreements) {
AccessModResult::Deny => denied = true,
AccessModResult::Constrain {
mut pres_attr,
mut rem_attr,
..
} => {
constrain_rem.append(&mut rem_attr);
constrain_pres.append(&mut pres_attr);
AccessResult::Denied => denied = true,
AccessResult::Constrain(mut set) => {
constrain_rem.extend(set.iter().cloned());
constrain_pres.append(&mut set)
}
// Can't grant.
// AccessModResult::Grant |
AccessResult::Grant |
// Can't allow
AccessModResult::Allow { .. } | AccessModResult::Ignore => {}
AccessResult::Allow(_) |
AccessResult::Ignore => {}
}
// Setup the acp's here
@ -156,27 +122,35 @@ pub(super) fn apply_modify_access<'a>(
.collect();
match modify_pres_test(scoped_acp.as_slice()) {
AccessModResult::Deny => denied = true,
AccessResult::Denied => denied = true,
// Can never return a unilateral grant.
// AccessModResult::Grant => {}
AccessModResult::Ignore => {}
AccessModResult::Constrain { .. } => {}
AccessModResult::Allow {
mut pres_attr,
mut rem_attr,
mut pres_class,
mut rem_class,
} => {
allow_pres.append(&mut pres_attr);
allow_rem.append(&mut rem_attr);
allow_pres_cls.append(&mut pres_class);
allow_rem_cls.append(&mut rem_class);
}
AccessResult::Grant => {}
AccessResult::Ignore => {}
AccessResult::Constrain(mut set) => constrain_pres.append(&mut set),
AccessResult::Allow(mut set) => allow_pres.append(&mut set),
}
match modify_rem_test(scoped_acp.as_slice()) {
AccessResult::Denied => denied = true,
// Can never return a unilateral grant.
AccessResult::Grant => {}
AccessResult::Ignore => {}
AccessResult::Constrain(mut set) => constrain_rem.append(&mut set),
AccessResult::Allow(mut set) => allow_rem.append(&mut set),
}
match modify_cls_test(scoped_acp.as_slice()) {
AccessResultClass::Denied => denied = true,
// Can never return a unilateral grant.
AccessResultClass::Grant => {}
AccessResultClass::Ignore => {}
AccessResultClass::Constrain(mut set) => constrain_cls.append(&mut set),
AccessResultClass::Allow(mut set) => allow_cls.append(&mut set),
}
}
if denied {
ModifyResult::Deny
ModifyResult::Denied
} else if grant {
ModifyResult::Grant
} else {
@ -194,48 +168,31 @@ pub(super) fn apply_modify_access<'a>(
allow_rem
};
let mut allowed_pres_cls = if !constrain_pres_cls.is_empty() {
let allowed_cls = if !constrain_cls.is_empty() {
// bit_and
&constrain_pres_cls & &allow_pres_cls
&constrain_cls & &allow_cls
} else {
allow_pres_cls
allow_cls
};
let mut allowed_rem_cls = if !constrain_rem_cls.is_empty() {
// bit_and
&constrain_rem_cls & &allow_rem_cls
} else {
allow_rem_cls
};
// Deny these classes from being part of any addition or removal to an entry
for protected_cls in PROTECTED_MOD_PRES_ENTRY_CLASSES.iter() {
allowed_pres_cls.remove(protected_cls.as_str());
}
for protected_cls in PROTECTED_MOD_REM_ENTRY_CLASSES.iter() {
allowed_rem_cls.remove(protected_cls.as_str());
}
ModifyResult::Allow {
pres: allowed_pres,
rem: allowed_rem,
pres_cls: allowed_pres_cls,
rem_cls: allowed_rem_cls,
cls: allowed_cls,
}
}
}
fn modify_ident_test(ident: &Identity) -> AccessBasicResult {
fn modify_ident_test(ident: &Identity) -> AccessResult {
match &ident.origin {
IdentType::Internal => {
trace!("Internal operation, bypassing access check");
// No need to check ACS
return AccessBasicResult::Grant;
return AccessResult::Grant;
}
IdentType::Synch(_) => {
security_critical!("Blocking sync check");
return AccessBasicResult::Deny;
return AccessResult::Denied;
}
IdentType::User(_) => {}
};
@ -244,56 +201,53 @@ fn modify_ident_test(ident: &Identity) -> AccessBasicResult {
match ident.access_scope() {
AccessScope::ReadOnly | AccessScope::Synchronise => {
security_access!("denied ❌ - identity access scope is not permitted to modify");
return AccessBasicResult::Deny;
return AccessResult::Denied;
}
AccessScope::ReadWrite => {
// As you were
}
};
AccessBasicResult::Ignore
AccessResult::Ignore
}
fn modify_pres_test<'a>(scoped_acp: &[&'a AccessControlModify]) -> AccessModResult<'a> {
let pres_attr: BTreeSet<Attribute> = scoped_acp
fn modify_pres_test(scoped_acp: &[&AccessControlModify]) -> AccessResult {
let allowed_pres: BTreeSet<Attribute> = scoped_acp
.iter()
.flat_map(|acp| acp.presattrs.iter().cloned())
.collect();
AccessResult::Allow(allowed_pres)
}
let rem_attr: BTreeSet<Attribute> = scoped_acp
fn modify_rem_test(scoped_acp: &[&AccessControlModify]) -> AccessResult {
let allowed_rem: BTreeSet<Attribute> = scoped_acp
.iter()
.flat_map(|acp| acp.remattrs.iter().cloned())
.collect();
let pres_class: BTreeSet<&'a str> = scoped_acp
.iter()
.flat_map(|acp| acp.pres_classes.iter().map(|s| s.as_str()))
.collect();
let rem_class: BTreeSet<&'a str> = scoped_acp
.iter()
.flat_map(|acp| acp.rem_classes.iter().map(|s| s.as_str()))
.collect();
AccessModResult::Allow {
pres_attr,
rem_attr,
pres_class,
rem_class,
}
AccessResult::Allow(allowed_rem)
}
fn modify_sync_constrain<'a>(
// TODO: Should this be reverted to the Str borrow method? Or do we try to change
// to EntryClass?
fn modify_cls_test<'a>(scoped_acp: &[&'a AccessControlModify]) -> AccessResultClass<'a> {
let allowed_classes: BTreeSet<&'a str> = scoped_acp
.iter()
.flat_map(|acp| acp.classes.iter().map(|s| s.as_str()))
.collect();
AccessResultClass::Allow(allowed_classes)
}
fn modify_sync_constrain(
ident: &Identity,
entry: &Arc<EntrySealedCommitted>,
sync_agreements: &HashMap<Uuid, BTreeSet<Attribute>>,
) -> AccessModResult<'a> {
) -> AccessResult {
match &ident.origin {
IdentType::Internal => AccessModResult::Ignore,
IdentType::Internal => AccessResult::Ignore,
IdentType::Synch(_) => {
// Allowed to mod sync objects. Later we'll probably need to check the limits of what
// it can do if we go that way.
AccessModResult::Ignore
AccessResult::Ignore
}
IdentType::User(_) => {
// We need to meet these conditions.
@ -305,7 +259,7 @@ fn modify_sync_constrain<'a>(
.unwrap_or(false);
if !is_sync {
return AccessModResult::Ignore;
return AccessResult::Ignore;
}
if let Some(sync_uuid) = entry.get_ava_single_refer(Attribute::SyncParentUuid) {
@ -320,115 +274,11 @@ fn modify_sync_constrain<'a>(
set.extend(sync_yield_authority.iter().cloned())
}
AccessModResult::Constrain {
pres_attr: set.clone(),
rem_attr: set,
pres_cls: None,
rem_cls: None,
}
AccessResult::Constrain(set)
} else {
warn!(entry = ?entry.get_uuid(), "sync_parent_uuid not found on sync object, preventing all access");
AccessModResult::Deny
AccessResult::Denied
}
}
}
}
/// Verify if the modification runs into limits that are defined by our protection rules.
fn modify_protected_attrs<'a>(
ident: &Identity,
entry: &Arc<EntrySealedCommitted>,
) -> AccessModResult<'a> {
match &ident.origin {
IdentType::Internal | IdentType::Synch(_) => {
// We don't constraint or influence these.
AccessModResult::Ignore
}
IdentType::User(_) => {
if let Some(classes) = entry.get_ava_as_iutf8(Attribute::Class) {
if classes.is_disjoint(&PROTECTED_MOD_ENTRY_CLASSES) {
// Not protected, go ahead
AccessModResult::Ignore
} else {
// Okay, the entry is protected, apply the full ruleset.
modify_protected_entry_attrs(classes)
}
} else {
// Nothing to check - this entry will fail to modify anyway because it has
// no classes
AccessModResult::Ignore
}
}
}
}
fn modify_protected_entry_attrs<'a>(classes: &BTreeSet<String>) -> AccessModResult<'a> {
// This is where the majority of the logic is - this contains the modification
// rules as they apply.
// First check for the hard-deny rules.
if !classes.is_disjoint(&LOCKED_ENTRY_CLASSES) {
// Hard deny attribute modifications to these types.
return AccessModResult::Deny;
}
let mut constrain_attrs = BTreeSet::default();
// Allows removal of the recycled class specifically on recycled entries.
if classes.contains(EntryClass::Recycled.into()) {
constrain_attrs.extend([Attribute::Class]);
}
if classes.contains(EntryClass::ClassType.into()) {
constrain_attrs.extend([Attribute::May, Attribute::Must]);
}
if classes.contains(EntryClass::SystemConfig.into()) {
constrain_attrs.extend([Attribute::BadlistPassword]);
}
// Allow domain settings.
if classes.contains(EntryClass::DomainInfo.into()) {
constrain_attrs.extend([
Attribute::DomainSsid,
Attribute::DomainLdapBasedn,
Attribute::LdapMaxQueryableAttrs,
Attribute::LdapAllowUnixPwBind,
Attribute::FernetPrivateKeyStr,
Attribute::Es256PrivateKeyDer,
Attribute::KeyActionRevoke,
Attribute::KeyActionRotate,
Attribute::IdVerificationEcKey,
Attribute::DeniedName,
Attribute::DomainDisplayName,
Attribute::Image,
]);
}
// Allow account policy related attributes to be changed on dyngroup
if classes.contains(EntryClass::DynGroup.into()) {
constrain_attrs.extend([
Attribute::AuthSessionExpiry,
Attribute::AuthPasswordMinimumLength,
Attribute::CredentialTypeMinimum,
Attribute::PrivilegeExpiry,
Attribute::WebauthnAttestationCaList,
Attribute::LimitSearchMaxResults,
Attribute::LimitSearchMaxFilterTest,
Attribute::AllowPrimaryCredFallback,
]);
}
// If we don't constrain the attributes at all, we have to deny the change
// from proceeding.
if constrain_attrs.is_empty() {
AccessModResult::Deny
} else {
AccessModResult::Constrain {
pres_attr: constrain_attrs.clone(),
rem_attr: constrain_attrs,
pres_cls: None,
rem_cls: None,
}
}
}

View file

@ -266,10 +266,9 @@ pub struct AccessControlModifyResolved<'a> {
#[derive(Debug, Clone)]
pub struct AccessControlModify {
pub acp: AccessControlProfile,
pub classes: Vec<AttrString>,
pub presattrs: Vec<Attribute>,
pub remattrs: Vec<Attribute>,
pub pres_classes: Vec<AttrString>,
pub rem_classes: Vec<AttrString>,
}
impl AccessControlModify {
@ -294,25 +293,14 @@ impl AccessControlModify {
.map(|i| i.map(Attribute::from).collect())
.unwrap_or_default();
let classes: Vec<AttrString> = value
let classes = value
.get_ava_iter_iutf8(Attribute::AcpModifyClass)
.map(|i| i.map(AttrString::from).collect())
.unwrap_or_default();
let pres_classes = value
.get_ava_iter_iutf8(Attribute::AcpModifyPresentClass)
.map(|i| i.map(AttrString::from).collect())
.unwrap_or_else(|| classes.clone());
let rem_classes = value
.get_ava_iter_iutf8(Attribute::AcpModifyRemoveClass)
.map(|i| i.map(AttrString::from).collect())
.unwrap_or_else(|| classes);
Ok(AccessControlModify {
acp: AccessControlProfile::try_from(qs, value)?,
pres_classes,
rem_classes,
classes,
presattrs,
remattrs,
})
@ -328,8 +316,7 @@ impl AccessControlModify {
targetscope: Filter<FilterValid>,
presattrs: &str,
remattrs: &str,
pres_classes: &str,
rem_classes: &str,
classes: &str,
) -> Self {
AccessControlModify {
acp: AccessControlProfile {
@ -338,14 +325,7 @@ impl AccessControlModify {
receiver: AccessControlReceiver::Group(btreeset!(receiver)),
target: AccessControlTarget::Scope(targetscope),
},
pres_classes: pres_classes
.split_whitespace()
.map(AttrString::from)
.collect(),
rem_classes: rem_classes
.split_whitespace()
.map(AttrString::from)
.collect(),
classes: classes.split_whitespace().map(AttrString::from).collect(),
presattrs: presattrs.split_whitespace().map(Attribute::from).collect(),
remattrs: remattrs.split_whitespace().map(Attribute::from).collect(),
}
@ -360,8 +340,7 @@ impl AccessControlModify {
target: AccessControlTarget,
presattrs: &str,
remattrs: &str,
pres_classes: &str,
rem_classes: &str,
classes: &str,
) -> Self {
AccessControlModify {
acp: AccessControlProfile {
@ -370,14 +349,7 @@ impl AccessControlModify {
receiver: AccessControlReceiver::EntryManager,
target,
},
pres_classes: pres_classes
.split_whitespace()
.map(AttrString::from)
.collect(),
rem_classes: rem_classes
.split_whitespace()
.map(AttrString::from)
.collect(),
classes: classes.split_whitespace().map(AttrString::from).collect(),
presattrs: presattrs.split_whitespace().map(Attribute::from).collect(),
remattrs: remattrs.split_whitespace().map(Attribute::from).collect(),
}

View file

@ -1,83 +0,0 @@
use crate::prelude::EntryClass;
use std::collections::BTreeSet;
use std::sync::LazyLock;
/// These entry classes may not be created or deleted, and may invoke some protection rules
/// if on an entry.
pub static PROTECTED_ENTRY_CLASSES: LazyLock<BTreeSet<String>> = LazyLock::new(|| {
let classes = vec![
EntryClass::System,
EntryClass::DomainInfo,
EntryClass::SystemInfo,
EntryClass::SystemConfig,
EntryClass::DynGroup,
EntryClass::SyncObject,
EntryClass::Tombstone,
EntryClass::Recycled,
];
BTreeSet::from_iter(classes.into_iter().map(|ec| ec.into()))
});
/// Entries with these classes are protected from modifications - not that
/// sync object is not present here as there are separate rules for that in
/// the modification access module.
///
/// Recycled is also not protected here as it needs to be able to be removed
/// by a recycle bin admin.
pub static PROTECTED_MOD_ENTRY_CLASSES: LazyLock<BTreeSet<String>> = LazyLock::new(|| {
let classes = vec![
EntryClass::System,
EntryClass::DomainInfo,
EntryClass::SystemInfo,
EntryClass::SystemConfig,
EntryClass::DynGroup,
// EntryClass::SyncObject,
EntryClass::Tombstone,
EntryClass::Recycled,
];
BTreeSet::from_iter(classes.into_iter().map(|ec| ec.into()))
});
/// These classes may NOT be added to ANY ENTRY
pub static PROTECTED_MOD_PRES_ENTRY_CLASSES: LazyLock<BTreeSet<String>> = LazyLock::new(|| {
let classes = vec![
EntryClass::System,
EntryClass::DomainInfo,
EntryClass::SystemInfo,
EntryClass::SystemConfig,
EntryClass::DynGroup,
EntryClass::SyncObject,
EntryClass::Tombstone,
EntryClass::Recycled,
];
BTreeSet::from_iter(classes.into_iter().map(|ec| ec.into()))
});
/// These classes may NOT be removed from ANY ENTRY
pub static PROTECTED_MOD_REM_ENTRY_CLASSES: LazyLock<BTreeSet<String>> = LazyLock::new(|| {
let classes = vec![
EntryClass::System,
EntryClass::DomainInfo,
EntryClass::SystemInfo,
EntryClass::SystemConfig,
EntryClass::DynGroup,
EntryClass::SyncObject,
EntryClass::Tombstone,
// EntryClass::Recycled,
];
BTreeSet::from_iter(classes.into_iter().map(|ec| ec.into()))
});
/// Entries with these classes may not be modified under any circumstance.
pub static LOCKED_ENTRY_CLASSES: LazyLock<BTreeSet<String>> = LazyLock::new(|| {
let classes = vec![
EntryClass::Tombstone,
// EntryClass::Recycled,
];
BTreeSet::from_iter(classes.into_iter().map(|ec| ec.into()))
});

View file

@ -4,11 +4,11 @@ use std::collections::BTreeSet;
use super::profiles::{
AccessControlReceiverCondition, AccessControlSearchResolved, AccessControlTargetCondition,
};
use super::AccessSrchResult;
use super::AccessResult;
use std::sync::Arc;
pub(super) enum SearchResult {
Deny,
Denied,
Grant,
Allow(BTreeSet<Attribute>),
}
@ -23,32 +23,32 @@ pub(super) fn apply_search_access(
// that.
let mut denied = false;
let mut grant = false;
let constrain = BTreeSet::default();
let mut constrain = BTreeSet::default();
let mut allow = BTreeSet::default();
// The access control profile
match search_filter_entry(ident, related_acp, entry) {
AccessSrchResult::Deny => denied = true,
AccessSrchResult::Grant => grant = true,
AccessSrchResult::Ignore => {}
// AccessSrchResult::Constrain { mut attr } => constrain.append(&mut attr),
AccessSrchResult::Allow { mut attr } => allow.append(&mut attr),
AccessResult::Denied => denied = true,
AccessResult::Grant => grant = true,
AccessResult::Ignore => {}
AccessResult::Constrain(mut set) => constrain.append(&mut set),
AccessResult::Allow(mut set) => allow.append(&mut set),
};
match search_oauth2_filter_entry(ident, entry) {
AccessSrchResult::Deny => denied = true,
AccessSrchResult::Grant => grant = true,
AccessSrchResult::Ignore => {}
// AccessSrchResult::Constrain { mut attr } => constrain.append(&mut attr),
AccessSrchResult::Allow { mut attr } => allow.append(&mut attr),
AccessResult::Denied => denied = true,
AccessResult::Grant => grant = true,
AccessResult::Ignore => {}
AccessResult::Constrain(mut set) => constrain.append(&mut set),
AccessResult::Allow(mut set) => allow.append(&mut set),
};
match search_sync_account_filter_entry(ident, entry) {
AccessSrchResult::Deny => denied = true,
AccessSrchResult::Grant => grant = true,
AccessSrchResult::Ignore => {}
// AccessSrchResult::Constrain{ mut attr } => constrain.append(&mut attr),
AccessSrchResult::Allow { mut attr } => allow.append(&mut attr),
AccessResult::Denied => denied = true,
AccessResult::Grant => grant = true,
AccessResult::Ignore => {}
AccessResult::Constrain(mut set) => constrain.append(&mut set),
AccessResult::Allow(mut set) => allow.append(&mut set),
};
// We'll add more modules later.
@ -56,7 +56,7 @@ pub(super) fn apply_search_access(
// Now finalise the decision.
if denied {
SearchResult::Deny
SearchResult::Denied
} else if grant {
SearchResult::Grant
} else {
@ -74,17 +74,17 @@ fn search_filter_entry(
ident: &Identity,
related_acp: &[AccessControlSearchResolved],
entry: &Arc<EntrySealedCommitted>,
) -> AccessSrchResult {
) -> AccessResult {
// If this is an internal search, return our working set.
match &ident.origin {
IdentType::Internal => {
trace!(uuid = ?entry.get_display_id(), "Internal operation, bypassing access check");
// No need to check ACS
return AccessSrchResult::Grant;
return AccessResult::Grant;
}
IdentType::Synch(_) => {
security_debug!(uuid = ?entry.get_display_id(), "Blocking sync check");
return AccessSrchResult::Deny;
return AccessResult::Denied;
}
IdentType::User(_) => {}
};
@ -95,7 +95,7 @@ fn search_filter_entry(
security_debug!(
"denied ❌ - identity access scope 'Synchronise' is not permitted to search"
);
return AccessSrchResult::Deny;
return AccessResult::Denied;
}
AccessScope::ReadOnly | AccessScope::ReadWrite => {
// As you were
@ -161,21 +161,16 @@ fn search_filter_entry(
.flatten()
.collect();
AccessSrchResult::Allow {
attr: allowed_attrs,
}
AccessResult::Allow(allowed_attrs)
}
fn search_oauth2_filter_entry(
ident: &Identity,
entry: &Arc<EntrySealedCommitted>,
) -> AccessSrchResult {
fn search_oauth2_filter_entry(ident: &Identity, entry: &Arc<EntrySealedCommitted>) -> AccessResult {
match &ident.origin {
IdentType::Internal | IdentType::Synch(_) => AccessSrchResult::Ignore,
IdentType::Internal | IdentType::Synch(_) => AccessResult::Ignore,
IdentType::User(iuser) => {
if iuser.entry.get_uuid() == UUID_ANONYMOUS {
debug!("Anonymous can't access OAuth2 entries, ignoring");
return AccessSrchResult::Ignore;
return AccessResult::Ignore;
}
let contains_o2_rs = entry
@ -195,18 +190,16 @@ fn search_oauth2_filter_entry(
if contains_o2_rs && contains_o2_scope_member {
security_debug!(entry = ?entry.get_uuid(), ident = ?iuser.entry.get_uuid2rdn(), "ident is a memberof a group granted an oauth2 scope by this entry");
return AccessSrchResult::Allow {
attr: btreeset!(
Attribute::Class,
Attribute::DisplayName,
Attribute::Uuid,
Attribute::Name,
Attribute::OAuth2RsOriginLanding,
Attribute::Image
),
};
return AccessResult::Allow(btreeset!(
Attribute::Class,
Attribute::DisplayName,
Attribute::Uuid,
Attribute::Name,
Attribute::OAuth2RsOriginLanding,
Attribute::Image
));
}
AccessSrchResult::Ignore
AccessResult::Ignore
}
}
}
@ -214,9 +207,9 @@ fn search_oauth2_filter_entry(
fn search_sync_account_filter_entry(
ident: &Identity,
entry: &Arc<EntrySealedCommitted>,
) -> AccessSrchResult {
) -> AccessResult {
match &ident.origin {
IdentType::Internal | IdentType::Synch(_) => AccessSrchResult::Ignore,
IdentType::Internal | IdentType::Synch(_) => AccessResult::Ignore,
IdentType::User(iuser) => {
// Is the user a synced object?
let is_user_sync_account = iuser
@ -251,18 +244,16 @@ fn search_sync_account_filter_entry(
// We finally got here!
security_debug!(entry = ?entry.get_uuid(), ident = ?iuser.entry.get_uuid2rdn(), "ident is a synchronised account from this sync account");
return AccessSrchResult::Allow {
attr: btreeset!(
Attribute::Class,
Attribute::Uuid,
Attribute::SyncCredentialPortal
),
};
return AccessResult::Allow(btreeset!(
Attribute::Class,
Attribute::Uuid,
Attribute::SyncCredentialPortal
));
}
}
}
// Fall through
AccessSrchResult::Ignore
AccessResult::Ignore
}
}
}

View file

@ -31,7 +31,7 @@ use crate::value::{CredentialType, EXTRACT_VAL_DN};
use crate::valueset::uuid_to_proto_string;
use crate::valueset::ScimValueIntermediate;
use crate::valueset::*;
use concread::arcache::{ARCacheBuilder, ARCacheReadTxn, ARCacheWriteTxn};
use concread::arcache::{ARCacheBuilder, ARCacheReadTxn};
use concread::cowcell::*;
use hashbrown::{HashMap, HashSet};
use kanidm_proto::internal::{DomainInfo as ProtoDomainInfo, ImageValue, UiHint};
@ -205,13 +205,6 @@ pub struct QueryServerWriteTransaction<'a> {
pub(super) changed_uuid: HashSet<Uuid>,
_db_ticket: SemaphorePermit<'a>,
_write_ticket: SemaphorePermit<'a>,
resolve_filter_cache_clear: bool,
resolve_filter_cache_write: ARCacheWriteTxn<
'a,
(IdentityId, Arc<Filter<FilterValid>>),
Arc<Filter<FilterValidResolved>>,
(),
>,
resolve_filter_cache: ARCacheReadTxn<
'a,
(IdentityId, Arc<Filter<FilterValid>>),
@ -267,7 +260,7 @@ pub trait QueryServerTransaction<'a> {
fn get_domain_image_value(&self) -> Option<ImageValue>;
fn get_resolve_filter_cache(&mut self) -> Option<&mut ResolveFilterCacheReadTxn<'a>>;
fn get_resolve_filter_cache(&mut self) -> &mut ResolveFilterCacheReadTxn<'a>;
// Because of how borrowck in rust works, if we need to get two inner types we have to get them
// in a single fn.
@ -276,7 +269,7 @@ pub trait QueryServerTransaction<'a> {
&mut self,
) -> (
&mut Self::BackendTransactionType,
Option<&mut ResolveFilterCacheReadTxn<'a>>,
&mut ResolveFilterCacheReadTxn<'a>,
);
/// Conduct a search and apply access controls to yield a set of entries that
@ -333,15 +326,11 @@ pub trait QueryServerTransaction<'a> {
// NOTE: Filters are validated in event conversion.
let (be_txn, resolve_filter_cache) = self.get_resolve_filter_cache_and_be_txn();
let idxmeta = be_txn.get_idxmeta_ref();
trace!(resolve_filter_cache = %resolve_filter_cache.is_some());
// Now resolve all references and indexes.
let vfr = se
.filter
.resolve(&se.ident, Some(idxmeta), resolve_filter_cache)
.resolve(&se.ident, Some(idxmeta), Some(resolve_filter_cache))
.map_err(|e| {
admin_error!(?e, "search filter resolve failure");
e
@ -377,7 +366,7 @@ pub trait QueryServerTransaction<'a> {
let vfr = ee
.filter
.resolve(&ee.ident, Some(idxmeta), resolve_filter_cache)
.resolve(&ee.ident, Some(idxmeta), Some(resolve_filter_cache))
.map_err(|e| {
admin_error!(?e, "Failed to resolve filter");
e
@ -1455,17 +1444,17 @@ impl<'a> QueryServerTransaction<'a> for QueryServerReadTransaction<'a> {
&self.key_providers
}
fn get_resolve_filter_cache(&mut self) -> Option<&mut ResolveFilterCacheReadTxn<'a>> {
Some(&mut self.resolve_filter_cache)
fn get_resolve_filter_cache(&mut self) -> &mut ResolveFilterCacheReadTxn<'a> {
&mut self.resolve_filter_cache
}
fn get_resolve_filter_cache_and_be_txn(
&mut self,
) -> (
&mut BackendReadTransaction<'a>,
Option<&mut ResolveFilterCacheReadTxn<'a>>,
&mut ResolveFilterCacheReadTxn<'a>,
) {
(&mut self.be_txn, Some(&mut self.resolve_filter_cache))
(&mut self.be_txn, &mut self.resolve_filter_cache)
}
fn pw_badlist(&self) -> &HashSet<String> {
@ -1689,25 +1678,17 @@ impl<'a> QueryServerTransaction<'a> for QueryServerWriteTransaction<'a> {
&self.key_providers
}
fn get_resolve_filter_cache(&mut self) -> Option<&mut ResolveFilterCacheReadTxn<'a>> {
if self.resolve_filter_cache_clear || *self.phase < ServerPhase::SchemaReady {
None
} else {
Some(&mut self.resolve_filter_cache)
}
fn get_resolve_filter_cache(&mut self) -> &mut ResolveFilterCacheReadTxn<'a> {
&mut self.resolve_filter_cache
}
fn get_resolve_filter_cache_and_be_txn(
&mut self,
) -> (
&mut BackendWriteTransaction<'a>,
Option<&mut ResolveFilterCacheReadTxn<'a>>,
&mut ResolveFilterCacheReadTxn<'a>,
) {
if self.resolve_filter_cache_clear || *self.phase < ServerPhase::SchemaReady {
(&mut self.be_txn, None)
} else {
(&mut self.be_txn, Some(&mut self.resolve_filter_cache))
}
(&mut self.be_txn, &mut self.resolve_filter_cache)
}
fn pw_badlist(&self) -> &HashSet<String> {
@ -2022,8 +2003,6 @@ impl QueryServer {
_db_ticket: db_ticket,
_write_ticket: write_ticket,
resolve_filter_cache: self.resolve_filter_cache.read(),
resolve_filter_cache_clear: false,
resolve_filter_cache_write: self.resolve_filter_cache.write(),
dyngroup_cache: self.dyngroup_cache.write(),
key_providers: self.key_providers.write(),
})
@ -2173,13 +2152,16 @@ impl<'a> QueryServerWriteTransaction<'a> {
))
}?;
// Since we reloaded the schema, we need to reload the filter cache since it
// may have incorrect or outdated information about indexes now.
self.resolve_filter_cache_clear = true;
// TODO: Clear the filter resolve cache.
// currently we can't do this because of the limits of types with arccache txns. The only
// thing this impacts is if something in indexed though, and the backend does handle
// incorrectly indexed items correctly.
// Trigger reloads on services that require post-schema reloads.
// Mainly this is plugins.
DynGroup::reload(self)?;
if *self.phase >= ServerPhase::SchemaReady {
DynGroup::reload(self)?;
}
Ok(())
}
@ -2602,14 +2584,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
self.changed_flags.remove(ChangeFlag::OAUTH2)
}
/// Indicate that we are about to re-bootstrap this server. You should ONLY
/// call this during a replication refresh!!!
pub(crate) fn set_phase_bootstrap(&mut self) {
*self.phase = ServerPhase::Bootstrap;
}
/// Raise the currently running server phase.
pub(crate) fn set_phase(&mut self, phase: ServerPhase) {
fn set_phase(&mut self, phase: ServerPhase) {
// Phase changes are one way
if phase > *self.phase {
*self.phase = phase
@ -2723,8 +2698,6 @@ impl<'a> QueryServerWriteTransaction<'a> {
changed_flags,
changed_uuid: _,
resolve_filter_cache: _,
resolve_filter_cache_clear,
mut resolve_filter_cache_write,
} = self;
debug_assert!(!committed);
@ -2738,12 +2711,6 @@ impl<'a> QueryServerWriteTransaction<'a> {
be_txn.set_db_ts_max(cid.ts)?;
cid.commit();
// We don't care if this passes/fails, committing this is fine.
if resolve_filter_cache_clear {
resolve_filter_cache_write.clear();
}
resolve_filter_cache_write.commit();
// Point of no return - everything has been validated and reloaded.
//
// = Lets commit =

View file

@ -388,65 +388,6 @@ impl fmt::Display for SyntaxType {
}
}
impl SyntaxType {
pub fn index_types(&self) -> &[IndexType] {
match self {
SyntaxType::Utf8String => &[IndexType::Equality, IndexType::Presence],
// Used by classes, needs to change ...
// Probably need an attrname syntax too
SyntaxType::Utf8StringInsensitive => &[IndexType::Equality, IndexType::Presence],
SyntaxType::Utf8StringIname => &[
IndexType::Equality,
IndexType::Presence,
IndexType::SubString,
],
SyntaxType::Uuid => &[IndexType::Equality, IndexType::Presence],
SyntaxType::Boolean => &[IndexType::Equality],
SyntaxType::SyntaxId => &[],
SyntaxType::IndexId => &[],
SyntaxType::ReferenceUuid => &[IndexType::Equality, IndexType::Presence],
SyntaxType::JsonFilter => &[],
SyntaxType::Credential => &[IndexType::Equality],
SyntaxType::SecretUtf8String => &[],
SyntaxType::SshKey => &[IndexType::Equality, IndexType::Presence],
SyntaxType::SecurityPrincipalName => &[
IndexType::Equality,
IndexType::Presence,
IndexType::SubString,
],
SyntaxType::Uint32 => &[IndexType::Equality, IndexType::Presence],
SyntaxType::Cid => &[],
SyntaxType::NsUniqueId => &[IndexType::Equality, IndexType::Presence],
SyntaxType::DateTime => &[],
SyntaxType::EmailAddress => &[IndexType::Equality, IndexType::SubString],
SyntaxType::Url => &[],
SyntaxType::OauthScope => &[],
SyntaxType::OauthScopeMap => &[IndexType::Equality],
SyntaxType::PrivateBinary => &[],
SyntaxType::IntentToken => &[IndexType::Equality],
SyntaxType::Passkey => &[IndexType::Equality],
SyntaxType::AttestedPasskey => &[IndexType::Equality],
SyntaxType::Session => &[IndexType::Equality],
SyntaxType::JwsKeyEs256 => &[],
SyntaxType::JwsKeyRs256 => &[],
SyntaxType::Oauth2Session => &[IndexType::Equality],
SyntaxType::UiHint => &[],
SyntaxType::TotpSecret => &[],
SyntaxType::ApiToken => &[IndexType::Equality],
SyntaxType::AuditLogString => &[],
SyntaxType::EcKeyPrivate => &[],
SyntaxType::Image => &[],
SyntaxType::CredentialType => &[],
SyntaxType::WebauthnAttestationCaList => &[],
SyntaxType::OauthClaimMap => &[IndexType::Equality],
SyntaxType::KeyInternal => &[],
SyntaxType::HexString => &[],
SyntaxType::Certificate => &[],
SyntaxType::ApplicationPassword => &[IndexType::Equality],
}
}
}
#[derive(
Hash,
Debug,

View file

@ -14,73 +14,87 @@ const ALLOWED_ATTRIBUTES: &[&str] = &[
"role",
"output_mode",
"log_level",
"ldap",
];
#[derive(Default)]
struct Flags {
ldap: bool,
}
fn parse_attributes(
args: &TokenStream,
fn parse_knobs(
input: &syn::ItemFn,
) -> Result<(proc_macro2::TokenStream, Flags), syn::Error> {
let args: Punctuated<ExprAssign, syn::token::Comma> =
Punctuated::<ExprAssign, Token![,]>::parse_terminated.parse(args.clone())?;
let args_are_allowed = args.pairs().all(|p| {
ALLOWED_ATTRIBUTES.to_vec().contains(
&p.value()
.left
.span()
.source_text()
.unwrap_or_default()
.as_str(),
)
});
if !args_are_allowed {
let msg = "Invalid test config attribute. The following are allowed";
return Err(syn::Error::new_spanned(
input.sig.fn_token,
format!("{}: {}", msg, ALLOWED_ATTRIBUTES.join(", ")),
));
}
let mut flags = Flags::default();
let mut field_modifications = quote! {};
args.pairs().for_each(|p| {
match p
.value()
.left
.span()
.source_text()
server_config: &Punctuated<ExprAssign, syn::token::Comma>,
) -> TokenStream {
// If type mismatch occurs, the current rustc points to the last statement.
let (last_stmt_start_span, _last_stmt_end_span) = {
let mut last_stmt = input
.block
.stmts
.last()
.map(ToTokens::into_token_stream)
.unwrap_or_default()
.as_str()
{
"ldap" => {
flags.ldap = true;
field_modifications.extend(quote! {
ldapbindaddress: Some("on".to_string()),})
}
_ => {
let field_name = p.value().left.to_token_stream(); // here we can use to_token_stream as we know we're iterating over ExprAssigns
let field_value = p.value().right.to_token_stream();
// This is printing out struct members.
field_modifications.extend(quote! {
#field_name: #field_value,})
}
}
.into_iter();
// `Span` on stable Rust has a limitation that only points to the first
// token, not the whole tokens. We can work around this limitation by
// using the first/last span of the tokens like
// `syn::Error::new_spanned` does.
let start = last_stmt.next().map_or_else(Span::call_site, |t| t.span());
let end = last_stmt.last().map_or(start, |t| t.span());
(start, end)
};
// here we gather all the provided configuration in a struct like declaration
// By now we have already checked that the configurations provided belong to the allowed subset
let mut field_modifications = quote! {};
server_config.pairs().for_each(|p| {
let field_name = p.value().left.to_token_stream(); // here we can use to_token_stream as we know we're iterating over ExprAssigns
let field_value = p.value().right.to_token_stream();
field_modifications.extend(quote! {
#field_name: #field_value,})
});
let ts = quote!(kanidmd_core::config::Configuration {
// Setup the config filling the remaining fields with the default values
let default_config_struct = quote!(kanidmd_core::config::Configuration {
#field_modifications
..kanidmd_core::config::Configuration::new_for_test()
});
Ok((ts, flags))
let rt = quote_spanned! {last_stmt_start_span=>
tokio::runtime::Builder::new_current_thread()
};
let header = quote! {
#[::core::prelude::v1::test]
};
let fn_name = &input.sig.ident;
let test_driver = Ident::new(&format!("tk_{}", fn_name), input.sig.span());
// Effectively we are just injecting a real test function around this which we will
// call.
let result = quote! {
#input
#header
fn #test_driver() {
let body = async {
let (rsclient, mut core_handle) = kanidmd_testkit::setup_async_test(#default_config_struct).await;
#fn_name(rsclient).await;
core_handle.shutdown().await;
};
#[allow(clippy::expect_used, clippy::diverging_sub_expression)]
{
return #rt
.enable_all()
.build()
.expect("Failed building the Runtime")
.block_on(body);
}
}
};
result.into()
}
fn token_stream_with_error(mut tokens: TokenStream, error: syn::Error) -> TokenStream {
tokens.extend(TokenStream::from(error.into_compile_error()));
tokens
}
pub(crate) fn test(args: TokenStream, item: TokenStream) -> TokenStream {
@ -101,80 +115,31 @@ pub(crate) fn test(args: TokenStream, item: TokenStream) -> TokenStream {
let msg = "the `async` keyword is missing from the function declaration";
return token_stream_with_error(item, syn::Error::new_spanned(input.sig.fn_token, msg));
}
// If type mismatch occurs, the current rustc points to the last statement.
let (last_stmt_start_span, _last_stmt_end_span) = {
let mut last_stmt = input
.block
.stmts
.last()
.map(ToTokens::into_token_stream)
.unwrap_or_default()
.into_iter();
// `Span` on stable Rust has a limitation that only points to the first
// token, not the whole tokens. We can work around this limitation by
// using the first/last span of the tokens like
// `syn::Error::new_spanned` does.
let start = last_stmt.next().map_or_else(Span::call_site, |t| t.span());
let end = last_stmt.last().map_or(start, |t| t.span());
(start, end)
};
// Setup the config filling the remaining fields with the default values
let (default_config_struct, flags) = match parse_attributes(&args, &input) {
Ok(dc) => dc,
Err(e) => return token_stream_with_error(args, e),
};
let rt = quote_spanned! {last_stmt_start_span=>
tokio::runtime::Builder::new_current_thread()
};
let header = quote! {
#[::core::prelude::v1::test]
};
let test_fn_args = if flags.ldap {
quote! {
&test_env
}
} else {
quote! {
&test_env.rsclient
}
};
let test_fn = &input.sig.ident;
let test_driver = Ident::new(&format!("tk_{}", test_fn), input.sig.span());
// Effectively we are just injecting a real test function around this which we will
// call.
let result = quote! {
#input
#header
fn #test_driver() {
let body = async {
let mut test_env = kanidmd_testkit::setup_async_test(#default_config_struct).await;
#test_fn(#test_fn_args).await;
test_env.core_handle.shutdown().await;
};
#[allow(clippy::expect_used, clippy::diverging_sub_expression)]
{
return #rt
.enable_all()
.build()
.expect("Failed building the Runtime")
.block_on(body);
}
}
};
result.into()
}
fn token_stream_with_error(mut tokens: TokenStream, error: syn::Error) -> TokenStream {
tokens.extend(TokenStream::from(error.into_compile_error()));
tokens
let args: Punctuated<ExprAssign, syn::token::Comma> =
match Punctuated::<ExprAssign, Token![,]>::parse_terminated.parse(args.clone()) {
Ok(it) => it,
Err(e) => return token_stream_with_error(args, e),
};
let args_are_allowed = args.pairs().all(|p| {
ALLOWED_ATTRIBUTES.to_vec().contains(
&p.value()
.left
.span()
.source_text()
.unwrap_or_default()
.as_str(),
)
});
if !args_are_allowed {
let msg =
"Currently only a subset of all the server configs can be set. Here is the full list";
return token_stream_with_error(
item,
syn::Error::new_spanned(
input.sig.fn_token,
format!("{}: {}", msg, ALLOWED_ATTRIBUTES.join(", ")),
),
);
}
parse_knobs(&input, &args)
}

View file

@ -51,9 +51,8 @@ kanidm_build_profiles = { workspace = true }
compact_jwt = { workspace = true }
escargot = "0.5.13"
# used for webdriver testing
fantoccini = { version = "0.21.5" }
fantoccini = { version = "0.21.4" }
futures = { workspace = true }
ldap3_client = { workspace = true }
oauth2_ext = { workspace = true, default-features = false, features = [
"reqwest",
] }
@ -64,7 +63,7 @@ tokio-openssl = { workspace = true }
kanidm_lib_crypto = { workspace = true }
uuid = { workspace = true }
webauthn-authenticator-rs = { workspace = true }
jsonschema = "0.29.1"
jsonschema = "0.29.0"
[package.metadata.cargo-machete]
ignored = ["escargot", "futures", "kanidm_build_profiles"]

View file

@ -10,16 +10,16 @@
#![deny(clippy::needless_pass_by_value)]
#![deny(clippy::trivially_copy_pass_by_ref)]
use std::net::TcpStream;
use std::sync::atomic::{AtomicU16, Ordering};
use kanidm_client::{KanidmClient, KanidmClientBuilder};
use kanidm_proto::internal::{Filter, Modify, ModifyList};
use kanidmd_core::config::{Configuration, IntegrationTestConfig};
use kanidmd_core::{create_server_core, CoreHandle};
use kanidmd_lib::prelude::{Attribute, NAME_SYSTEM_ADMINS};
use std::net::TcpStream;
use std::sync::atomic::{AtomicU16, Ordering};
use tokio::task;
use tracing::error;
use url::Url;
pub const ADMIN_TEST_USER: &str = "admin";
pub const ADMIN_TEST_PASSWORD: &str = "integration test admin password";
@ -46,9 +46,14 @@ pub fn is_free_port(port: u16) -> bool {
}
// Test external behaviours of the service.
fn port_loop() -> u16 {
// allowed because the use of this function is behind a test gate
#[allow(dead_code)]
pub async fn setup_async_test(mut config: Configuration) -> (KanidmClient, CoreHandle) {
sketching::test_init();
let mut counter = 0;
loop {
let port = loop {
let possible_port = PORT_ALLOC.fetch_add(1, Ordering::SeqCst);
if is_free_port(possible_port) {
break possible_port;
@ -59,21 +64,7 @@ fn port_loop() -> u16 {
tracing::error!("Unable to allocate port!");
panic!();
}
}
}
pub struct AsyncTestEnvironment {
pub rsclient: KanidmClient,
pub core_handle: CoreHandle,
pub ldap_url: Option<Url>,
}
// allowed because the use of this function is behind a test gate
#[allow(dead_code)]
pub async fn setup_async_test(mut config: Configuration) -> AsyncTestEnvironment {
sketching::test_init();
let port = port_loop();
};
let int_config = Box::new(IntegrationTestConfig {
admin_user: ADMIN_TEST_USER.to_string(),
@ -84,16 +75,6 @@ pub async fn setup_async_test(mut config: Configuration) -> AsyncTestEnvironment
let addr = format!("http://localhost:{}", port);
let ldap_url = if config.ldapbindaddress.is_some() {
let ldapport = port_loop();
config.ldapbindaddress = Some(format!("127.0.0.1:{}", ldapport));
Url::parse(&format!("ldap://127.0.0.1:{}", ldapport))
.inspect_err(|err| error!(?err, "ldap address setup"))
.ok()
} else {
None
};
// Setup the address and origin..
config.address = format!("127.0.0.1:{}", port);
config.integration_test_config = Some(int_config);
@ -121,11 +102,7 @@ pub async fn setup_async_test(mut config: Configuration) -> AsyncTestEnvironment
tracing::info!("Testkit server setup complete - {}", addr);
AsyncTestEnvironment {
rsclient,
core_handle,
ldap_url,
}
(rsclient, core_handle)
}
/// creates a user (username: `id`) and puts them into a group, creating it if need be.

View file

@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use tracing::info;
#[kanidmd_testkit::test]
async fn check_that_the_swagger_api_loads(rsclient: &kanidm_client::KanidmClient) {
async fn check_that_the_swagger_api_loads(rsclient: kanidm_client::KanidmClient) {
#[derive(Serialize, Deserialize, Debug)]
struct OpenAPIResponse {
pub openapi: String,

View file

@ -3,7 +3,7 @@ use kanidm_proto::constants::ATTR_DOMAIN_DISPLAY_NAME;
use kanidmd_testkit::{ADMIN_TEST_PASSWORD, ADMIN_TEST_USER};
#[kanidmd_testkit::test]
async fn test_idm_set_ldap_allow_unix_password_bind(rsclient: &KanidmClient) {
async fn test_idm_set_ldap_allow_unix_password_bind(rsclient: KanidmClient) {
rsclient
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await
@ -13,9 +13,8 @@ async fn test_idm_set_ldap_allow_unix_password_bind(rsclient: &KanidmClient) {
.await
.expect("Failed to set LDAP allow unix password bind to true");
}
#[kanidmd_testkit::test]
async fn test_idm_domain_set_ldap_basedn(rsclient: &KanidmClient) {
async fn test_idm_domain_set_ldap_basedn(rsclient: KanidmClient) {
rsclient
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await
@ -28,7 +27,7 @@ async fn test_idm_domain_set_ldap_basedn(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_idm_domain_set_ldap_max_queryable_attrs(rsclient: &KanidmClient) {
async fn test_idm_domain_set_ldap_max_queryable_attrs(rsclient: KanidmClient) {
rsclient
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await
@ -41,7 +40,7 @@ async fn test_idm_domain_set_ldap_max_queryable_attrs(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_idm_domain_set_display_name(rsclient: &KanidmClient) {
async fn test_idm_domain_set_display_name(rsclient: KanidmClient) {
rsclient
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await

View file

@ -4,7 +4,7 @@ use kanidmd_testkit::{create_user, ADMIN_TEST_PASSWORD, ADMIN_TEST_USER};
use serde_json::Value;
#[kanidmd_testkit::test]
async fn test_v1_group_id_patch(rsclient: &KanidmClient) {
async fn test_v1_group_id_patch(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await;
@ -25,7 +25,7 @@ async fn test_v1_group_id_patch(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_v1_group_id_attr_post(rsclient: &KanidmClient) {
async fn test_v1_group_id_attr_post(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await;

View file

@ -1,7 +1,7 @@
use kanidm_client::{http::header, KanidmClient};
#[kanidmd_testkit::test]
async fn test_https_manifest(rsclient: &KanidmClient) {
async fn test_https_manifest(rsclient: KanidmClient) {
// We need to do manual reqwests here.
let client = rsclient.client();

View file

@ -11,7 +11,7 @@ const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
// *test where we don't trust the x-forwarded-for header
#[kanidmd_testkit::test(trust_x_forward_for = false)]
async fn dont_trust_xff_send_header(rsclient: &KanidmClient) {
async fn dont_trust_xff_send_header(rsclient: KanidmClient) {
let client = rsclient.client();
let res = client
@ -32,7 +32,7 @@ async fn dont_trust_xff_send_header(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test(trust_x_forward_for = false)]
async fn dont_trust_xff_dont_send_header(rsclient: &KanidmClient) {
async fn dont_trust_xff_dont_send_header(rsclient: KanidmClient) {
let client = rsclient.client();
let res = client
@ -58,7 +58,7 @@ async fn dont_trust_xff_dont_send_header(rsclient: &KanidmClient) {
// *test where we trust the x-forwarded-for header
#[kanidmd_testkit::test(trust_x_forward_for = true)]
async fn trust_xff_send_invalid_header_single_value(rsclient: &KanidmClient) {
async fn trust_xff_send_invalid_header_single_value(rsclient: KanidmClient) {
let client = rsclient.client();
let res = client
@ -78,7 +78,7 @@ async fn trust_xff_send_invalid_header_single_value(rsclient: &KanidmClient) {
// with a valid leftmost address and an invalid address later in the list. Right now it wouldn't work.
//
#[kanidmd_testkit::test(trust_x_forward_for = true)]
async fn trust_xff_send_invalid_header_multiple_values(rsclient: &KanidmClient) {
async fn trust_xff_send_invalid_header_multiple_values(rsclient: KanidmClient) {
let client = rsclient.client();
let res = client
@ -95,7 +95,7 @@ async fn trust_xff_send_invalid_header_multiple_values(rsclient: &KanidmClient)
}
#[kanidmd_testkit::test(trust_x_forward_for = true)]
async fn trust_xff_send_valid_header_single_ipv4_address(rsclient: &KanidmClient) {
async fn trust_xff_send_valid_header_single_ipv4_address(rsclient: KanidmClient) {
let ip_addr = "2001:db8:85a3:8d3:1319:8a2e:370:7348";
let client = rsclient.client();
@ -115,7 +115,7 @@ async fn trust_xff_send_valid_header_single_ipv4_address(rsclient: &KanidmClient
}
#[kanidmd_testkit::test(trust_x_forward_for = true)]
async fn trust_xff_send_valid_header_single_ipv6_address(rsclient: &KanidmClient) {
async fn trust_xff_send_valid_header_single_ipv6_address(rsclient: KanidmClient) {
let ip_addr = "203.0.113.195";
let client = rsclient.client();
@ -135,7 +135,7 @@ async fn trust_xff_send_valid_header_single_ipv6_address(rsclient: &KanidmClient
}
#[kanidmd_testkit::test(trust_x_forward_for = true)]
async fn trust_xff_send_valid_header_multiple_address(rsclient: &KanidmClient) {
async fn trust_xff_send_valid_header_multiple_address(rsclient: KanidmClient) {
let first_ip_addr = "203.0.113.195, 2001:db8:85a3:8d3:1319:8a2e:370:7348";
let client = rsclient.client();
@ -176,7 +176,7 @@ async fn trust_xff_send_valid_header_multiple_address(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test(trust_x_forward_for = true)]
async fn trust_xff_dont_send_header(rsclient: &KanidmClient) {
async fn trust_xff_dont_send_header(rsclient: KanidmClient) {
let client = rsclient.client();
let res = client

View file

@ -2,7 +2,7 @@ use kanidm_client::http::header;
use kanidm_client::KanidmClient;
#[kanidmd_testkit::test]
async fn test_https_middleware_headers(rsclient: &KanidmClient) {
async fn test_https_middleware_headers(rsclient: KanidmClient) {
// We need to do manual reqwests here.
let client = rsclient.client();

View file

@ -15,7 +15,7 @@ static USER_B_NAME: &str = "valid_user_b";
// These tests check that invalid requests return the expected error
#[kanidmd_testkit::test]
async fn test_not_authenticated(rsclient: &KanidmClient) {
async fn test_not_authenticated(rsclient: KanidmClient) {
// basically here we try a bit of all the possible combinations while unauthenticated to check it's not working
setup_server(&rsclient).await;
create_user(&rsclient, USER_A_NAME).await;
@ -46,7 +46,7 @@ async fn test_not_authenticated(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_non_existing_user_id(rsclient: &KanidmClient) {
async fn test_non_existing_user_id(rsclient: KanidmClient) {
setup_server(&rsclient).await;
create_user(&rsclient, USER_A_NAME).await;
create_user(&rsclient, USER_B_NAME).await;
@ -86,7 +86,7 @@ async fn test_non_existing_user_id(rsclient: &KanidmClient) {
// error cases have already been tested in the previous section!
// Each tests is named like `test_{api input}_response_{expected api output}_or_{expected api output}`
#[kanidmd_testkit::test]
async fn test_start_response_identity_verification_available(rsclient: &KanidmClient) {
async fn test_start_response_identity_verification_available(rsclient: KanidmClient) {
setup_server(&rsclient).await;
create_user(&rsclient, USER_A_NAME).await;
login_with_user(&rsclient, USER_A_NAME).await;
@ -105,7 +105,7 @@ async fn test_start_response_identity_verification_available(rsclient: &KanidmCl
// this function tests both possible POSITIVE outcomes if we start from
// `Start`, that is WaitForCode or ProvideCode
#[kanidmd_testkit::test]
async fn test_start_response_wait_for_code_or_provide_code(rsclient: &KanidmClient) {
async fn test_start_response_wait_for_code_or_provide_code(rsclient: KanidmClient) {
setup_server(&rsclient).await;
let user_a_uuid = create_user(&rsclient, USER_A_NAME).await;
let user_b_uuid = create_user(&rsclient, USER_B_NAME).await;
@ -129,7 +129,7 @@ async fn test_start_response_wait_for_code_or_provide_code(rsclient: &KanidmClie
}
#[kanidmd_testkit::test]
async fn test_provide_code_response_code_failure_or_provide_code(rsclient: &KanidmClient) {
async fn test_provide_code_response_code_failure_or_provide_code(rsclient: KanidmClient) {
setup_server(&rsclient).await;
let user_a_uuid = create_user(&rsclient, USER_A_NAME).await;
let user_b_uuid = create_user(&rsclient, USER_B_NAME).await;
@ -157,7 +157,7 @@ async fn test_provide_code_response_code_failure_or_provide_code(rsclient: &Kani
// here we actually test the full idm flow by duplicating the server
#[kanidmd_testkit::test]
async fn test_full_identification_flow(rsclient: &KanidmClient) {
async fn test_full_identification_flow(rsclient: KanidmClient) {
setup_server(&rsclient).await;
let user_a_uuid = create_user(&rsclient, USER_A_NAME).await;
let user_b_uuid = create_user(&rsclient, USER_B_NAME).await;
@ -175,12 +175,12 @@ async fn test_full_identification_flow(rsclient: &KanidmClient) {
(
valid_user_a_client,
USER_A_NAME,
&valid_user_b_client,
valid_user_b_client,
USER_B_NAME,
)
} else {
(
&valid_user_b_client,
valid_user_b_client,
USER_B_NAME,
valid_user_a_client,
USER_A_NAME,

View file

@ -66,7 +66,7 @@ async fn get_webdriver_client() -> fantoccini::Client {
#[kanidmd_testkit::test]
#[cfg(feature = "webdriver")]
async fn test_webdriver_user_login(rsclient: &KanidmClient) {
async fn test_webdriver_user_login(rsclient: kanidm_client::KanidmClient) {
if !cfg!(feature = "webdriver") {
println!("Skipping test as webdriver feature is not enabled!");
return;
@ -206,7 +206,7 @@ async fn test_webdriver_user_login(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_domain_reset_token_key(rsclient: &KanidmClient) {
async fn test_domain_reset_token_key(rsclient: KanidmClient) {
login_put_admin_idm_admins(&rsclient).await;
let token = rsclient.get_token().await.expect("No bearer token present");
@ -219,7 +219,7 @@ async fn test_domain_reset_token_key(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_idm_domain_set_ldap_basedn(rsclient: &KanidmClient) {
async fn test_idm_domain_set_ldap_basedn(rsclient: KanidmClient) {
login_put_admin_idm_admins(&rsclient).await;
assert!(rsclient
.idm_domain_set_ldap_basedn("dc=krabsarekool,dc=example,dc=com")
@ -232,7 +232,7 @@ async fn test_idm_domain_set_ldap_basedn(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_idm_domain_set_ldap_max_queryable_attrs(rsclient: &KanidmClient) {
async fn test_idm_domain_set_ldap_max_queryable_attrs(rsclient: KanidmClient) {
login_put_admin_idm_admins(&rsclient).await;
assert!(rsclient
.idm_domain_set_ldap_max_queryable_attrs(20)
@ -246,7 +246,7 @@ async fn test_idm_domain_set_ldap_max_queryable_attrs(rsclient: &KanidmClient) {
#[kanidmd_testkit::test]
/// Checks that a built-in group idm_all_persons has the "builtin" class as expected.
async fn test_all_persons_has_builtin_class(rsclient: &KanidmClient) {
async fn test_all_persons_has_builtin_class(rsclient: KanidmClient) {
login_put_admin_idm_admins(&rsclient).await;
let res = rsclient
.idm_group_get("idm_all_persons")

View file

@ -1,16 +0,0 @@
use kanidmd_testkit::AsyncTestEnvironment;
use ldap3_client::LdapClientBuilder;
#[kanidmd_testkit::test(ldap = true)]
async fn test_ldap_basic_unix_bind(test_env: &AsyncTestEnvironment) {
let ldap_url = test_env.ldap_url.as_ref().unwrap();
let mut ldap_client = LdapClientBuilder::new(ldap_url).build().await.unwrap();
// Bind as anonymous
ldap_client.bind("".into(), "".into()).await.unwrap();
let whoami = ldap_client.whoami().await.unwrap();
assert_eq!(whoami, Some("u: anonymous@localhost".to_string()));
}

View file

@ -6,7 +6,6 @@ mod https_extractors;
mod https_middleware;
mod identity_verification_tests;
mod integration;
mod ldap_basic;
mod mtls_test;
mod oauth2_test;
mod person;

View file

@ -40,7 +40,7 @@ use kanidmd_testkit::{
/// If `true`, use the `code` passed in the callback URI's fragment, and
/// require the query parameter to be empty.
async fn test_oauth2_openid_basic_flow_impl(
rsclient: &KanidmClient,
rsclient: KanidmClient,
response_mode: Option<&str>,
response_in_fragment: bool,
) {
@ -535,7 +535,7 @@ async fn test_oauth2_openid_basic_flow_impl(
///
/// The response should be returned as a query parameter.
#[kanidmd_testkit::test]
async fn test_oauth2_openid_basic_flow_mode_unset(rsclient: &KanidmClient) {
async fn test_oauth2_openid_basic_flow_mode_unset(rsclient: KanidmClient) {
test_oauth2_openid_basic_flow_impl(rsclient, None, false).await;
}
@ -544,7 +544,7 @@ async fn test_oauth2_openid_basic_flow_mode_unset(rsclient: &KanidmClient) {
///
/// The response should be returned as a query parameter.
#[kanidmd_testkit::test]
async fn test_oauth2_openid_basic_flow_mode_query(rsclient: &KanidmClient) {
async fn test_oauth2_openid_basic_flow_mode_query(rsclient: KanidmClient) {
test_oauth2_openid_basic_flow_impl(rsclient, Some("query"), false).await;
}
@ -553,7 +553,7 @@ async fn test_oauth2_openid_basic_flow_mode_query(rsclient: &KanidmClient) {
///
/// The response should be returned in the URI's fragment.
#[kanidmd_testkit::test]
async fn test_oauth2_openid_basic_flow_mode_fragment(rsclient: &KanidmClient) {
async fn test_oauth2_openid_basic_flow_mode_fragment(rsclient: KanidmClient) {
test_oauth2_openid_basic_flow_impl(rsclient, Some("fragment"), true).await;
}
@ -570,7 +570,7 @@ async fn test_oauth2_openid_basic_flow_mode_fragment(rsclient: &KanidmClient) {
/// If `true`, use the `code` passed in the callback URI's fragment, and
/// require the query parameter to be empty.
async fn test_oauth2_openid_public_flow_impl(
rsclient: &KanidmClient,
rsclient: KanidmClient,
response_mode: Option<&str>,
response_in_fragment: bool,
) {
@ -901,7 +901,7 @@ async fn test_oauth2_openid_public_flow_impl(
///
/// The response should be returned as a query parameter.
#[kanidmd_testkit::test]
async fn test_oauth2_openid_public_flow_mode_unset(rsclient: &KanidmClient) {
async fn test_oauth2_openid_public_flow_mode_unset(rsclient: KanidmClient) {
test_oauth2_openid_public_flow_impl(rsclient, None, false).await;
}
@ -910,7 +910,7 @@ async fn test_oauth2_openid_public_flow_mode_unset(rsclient: &KanidmClient) {
///
/// The response should be returned as a query parameter.
#[kanidmd_testkit::test]
async fn test_oauth2_openid_public_flow_mode_query(rsclient: &KanidmClient) {
async fn test_oauth2_openid_public_flow_mode_query(rsclient: KanidmClient) {
test_oauth2_openid_public_flow_impl(rsclient, Some("query"), false).await;
}
@ -919,12 +919,12 @@ async fn test_oauth2_openid_public_flow_mode_query(rsclient: &KanidmClient) {
///
/// The response should be returned in the URI's fragment.
#[kanidmd_testkit::test]
async fn test_oauth2_openid_public_flow_mode_fragment(rsclient: &KanidmClient) {
async fn test_oauth2_openid_public_flow_mode_fragment(rsclient: KanidmClient) {
test_oauth2_openid_public_flow_impl(rsclient, Some("fragment"), true).await;
}
#[kanidmd_testkit::test]
async fn test_oauth2_token_post_bad_bodies(rsclient: &KanidmClient) {
async fn test_oauth2_token_post_bad_bodies(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await;
@ -960,7 +960,7 @@ async fn test_oauth2_token_post_bad_bodies(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_oauth2_token_revoke_post(rsclient: &KanidmClient) {
async fn test_oauth2_token_revoke_post(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await;

View file

@ -4,7 +4,7 @@ use kanidmd_testkit::{create_user, ADMIN_TEST_PASSWORD, ADMIN_TEST_USER};
use serde_json::Value;
#[kanidmd_testkit::test]
async fn test_v1_person_id_patch(rsclient: &KanidmClient) {
async fn test_v1_person_id_patch(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await;
@ -25,7 +25,7 @@ async fn test_v1_person_id_patch(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_v1_person_id_ssh_pubkeys_post(rsclient: &KanidmClient) {
async fn test_v1_person_id_ssh_pubkeys_post(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await;

View file

@ -29,7 +29,7 @@ use kanidmd_testkit::{ADMIN_TEST_PASSWORD, ADMIN_TEST_USER};
const UNIX_TEST_PASSWORD: &str = "unix test user password";
#[kanidmd_testkit::test]
async fn test_server_create(rsclient: &KanidmClient) {
async fn test_server_create(rsclient: KanidmClient) {
let e: Entry = serde_json::from_str(
r#"{
"attrs": {
@ -55,7 +55,7 @@ async fn test_server_create(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_server_whoami_anonymous(rsclient: &KanidmClient) {
async fn test_server_whoami_anonymous(rsclient: KanidmClient) {
// First show we are un-authenticated.
let pre_res = rsclient.whoami().await;
// This means it was okay whoami, but no uat attached.
@ -84,7 +84,7 @@ async fn test_server_whoami_anonymous(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_server_whoami_admin_simple_password(rsclient: &KanidmClient) {
async fn test_server_whoami_admin_simple_password(rsclient: KanidmClient) {
// First show we are un-authenticated.
let pre_res = rsclient.whoami().await;
// This means it was okay whoami, but no uat attached.
@ -109,7 +109,7 @@ async fn test_server_whoami_admin_simple_password(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_server_search(rsclient: &KanidmClient) {
async fn test_server_search(rsclient: KanidmClient) {
// First show we are un-authenticated.
let pre_res = rsclient.whoami().await;
// This means it was okay whoami, but no uat attached.
@ -135,7 +135,7 @@ async fn test_server_search(rsclient: &KanidmClient) {
// test the rest group endpoint.
#[kanidmd_testkit::test]
async fn test_server_rest_group_read(rsclient: &KanidmClient) {
async fn test_server_rest_group_read(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
@ -151,7 +151,7 @@ async fn test_server_rest_group_read(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_server_rest_group_lifecycle(rsclient: &KanidmClient) {
async fn test_server_rest_group_lifecycle(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
@ -263,7 +263,7 @@ async fn test_server_rest_group_lifecycle(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_server_rest_account_read(rsclient: &KanidmClient) {
async fn test_server_rest_account_read(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
@ -279,7 +279,7 @@ async fn test_server_rest_account_read(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_server_rest_schema_read(rsclient: &KanidmClient) {
async fn test_server_rest_schema_read(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
@ -313,7 +313,7 @@ async fn test_server_rest_schema_read(rsclient: &KanidmClient) {
// Test resetting a radius cred, and then checking/viewing it.
#[kanidmd_testkit::test]
async fn test_server_radius_credential_lifecycle(rsclient: &KanidmClient) {
async fn test_server_radius_credential_lifecycle(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
@ -384,7 +384,7 @@ async fn test_server_radius_credential_lifecycle(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_server_rest_person_account_lifecycle(rsclient: &KanidmClient) {
async fn test_server_rest_person_account_lifecycle(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
@ -439,7 +439,7 @@ async fn test_server_rest_person_account_lifecycle(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_server_rest_sshkey_lifecycle(rsclient: &KanidmClient) {
async fn test_server_rest_sshkey_lifecycle(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
@ -509,7 +509,7 @@ async fn test_server_rest_sshkey_lifecycle(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_server_rest_domain_lifecycle(rsclient: &KanidmClient) {
async fn test_server_rest_domain_lifecycle(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
@ -539,7 +539,7 @@ async fn test_server_rest_domain_lifecycle(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_server_rest_posix_lifecycle(rsclient: &KanidmClient) {
async fn test_server_rest_posix_lifecycle(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
@ -660,7 +660,7 @@ async fn test_server_rest_posix_lifecycle(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_server_rest_posix_auth_lifecycle(rsclient: &KanidmClient) {
async fn test_server_rest_posix_auth_lifecycle(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
@ -760,7 +760,7 @@ async fn test_server_rest_posix_auth_lifecycle(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_server_rest_recycle_lifecycle(rsclient: &KanidmClient) {
async fn test_server_rest_recycle_lifecycle(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
@ -814,7 +814,7 @@ async fn test_server_rest_recycle_lifecycle(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_server_rest_oauth2_basic_lifecycle(rsclient: &KanidmClient) {
async fn test_server_rest_oauth2_basic_lifecycle(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
@ -1027,7 +1027,7 @@ async fn test_server_rest_oauth2_basic_lifecycle(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_server_credential_update_session_pw(rsclient: &KanidmClient) {
async fn test_server_credential_update_session_pw(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
@ -1102,7 +1102,7 @@ async fn test_server_credential_update_session_pw(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_server_credential_update_session_totp_pw(rsclient: &KanidmClient) {
async fn test_server_credential_update_session_totp_pw(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
@ -1365,7 +1365,7 @@ async fn setup_demo_account_password(
}
#[kanidmd_testkit::test]
async fn test_server_credential_update_session_passkey(rsclient: &KanidmClient) {
async fn test_server_credential_update_session_passkey(rsclient: KanidmClient) {
let mut wa = setup_demo_account_passkey(&rsclient).await;
let res = rsclient
@ -1383,7 +1383,7 @@ async fn test_server_credential_update_session_passkey(rsclient: &KanidmClient)
}
#[kanidmd_testkit::test]
async fn test_server_api_token_lifecycle(rsclient: &KanidmClient) {
async fn test_server_api_token_lifecycle(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await;
@ -1566,7 +1566,7 @@ async fn test_server_api_token_lifecycle(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_server_user_auth_token_lifecycle(rsclient: &KanidmClient) {
async fn test_server_user_auth_token_lifecycle(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await;
@ -1689,7 +1689,7 @@ async fn test_server_user_auth_token_lifecycle(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_server_user_auth_reauthentication(rsclient: &KanidmClient) {
async fn test_server_user_auth_reauthentication(rsclient: KanidmClient) {
let mut wa = setup_demo_account_passkey(&rsclient).await;
let res = rsclient
@ -1868,7 +1868,7 @@ async fn start_password_session(
}
#[kanidmd_testkit::test]
async fn test_server_user_auth_unprivileged(rsclient: &KanidmClient) {
async fn test_server_user_auth_unprivileged(rsclient: KanidmClient) {
let (account_name, account_pass) = setup_demo_account_password(&rsclient)
.await
.expect("Failed to setup demo_account");
@ -1891,7 +1891,7 @@ async fn test_server_user_auth_unprivileged(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_server_user_auth_privileged_shortcut(rsclient: &KanidmClient) {
async fn test_server_user_auth_privileged_shortcut(rsclient: KanidmClient) {
let (account_name, account_pass) = setup_demo_account_password(&rsclient)
.await
.expect("Failed to setup demo_account");

View file

@ -9,7 +9,7 @@ use std::str::FromStr;
use url::Url;
#[kanidmd_testkit::test]
async fn test_sync_account_lifecycle(rsclient: &KanidmClient) {
async fn test_sync_account_lifecycle(rsclient: KanidmClient) {
let a_res = rsclient
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await;
@ -104,7 +104,7 @@ async fn test_sync_account_lifecycle(rsclient: &KanidmClient) {
}
#[kanidmd_testkit::test]
async fn test_scim_sync_entry_get(rsclient: &KanidmClient) {
async fn test_scim_sync_entry_get(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;

View file

@ -2,7 +2,7 @@ use kanidm_client::KanidmClient;
/// This literally tests that the thing exists and responds in a way we expect, probably worth testing it better...
#[kanidmd_testkit::test]
async fn test_v1_service_account_id_attr_attr_delete(rsclient: &KanidmClient) {
async fn test_v1_service_account_id_attr_attr_delete(rsclient: KanidmClient) {
// We need to do manual reqwests here.
let client = rsclient.client();

View file

@ -2,7 +2,7 @@ use kanidm_client::KanidmClient;
/// This literally tests that the thing exists and responds in a way we expect, probably worth testing it better...
#[kanidmd_testkit::test]
async fn test_v1_system_post_attr(rsclient: &KanidmClient) {
async fn test_v1_system_post_attr(rsclient: KanidmClient) {
let client = rsclient.client();
let response = match client

View file

@ -3,8 +3,8 @@ use kanidmd_lib::constants::NAME_IDM_ADMINS;
use kanidmd_testkit::*;
#[kanidmd_testkit::test]
async fn account_id_unix_token(rsclient: &KanidmClient) {
login_put_admin_idm_admins(rsclient).await;
async fn account_id_unix_token(rsclient: KanidmClient) {
login_put_admin_idm_admins(&rsclient).await;
create_user(&rsclient, "group_manager", "idm_group_manage_priv").await;
// create test user without creating new groups

View file

@ -1,13 +1,14 @@
let
rust-overlay = (import (builtins.fetchTarball "https://github.com/oxalica/rust-overlay/archive/master.tar.gz"));
{ pkgs ? import <nixpkgs> {} }:
let
overrides = (builtins.fromTOML (builtins.readFile ./rust-toolchain.toml));
in
{ pkgs ? import <nixpkgs> { overlays = [ rust-overlay ]; } }:
pkgs.mkShellNoCC {
pkgs.mkShellNoCC rec {
# Kanidm dependencies
buildInputs = with pkgs; [
pkg-config
(rust-bin.fromRustupToolchainFile ./rust-toolchain.toml)
cargo
rustc
clang
llvmPackages.bintools
@ -18,6 +19,7 @@ pkgs.mkShellNoCC {
linux-pam
];
RUSTC_VERSION = overrides.toolchain.channel;
# https://github.com/rust-lang/rust-bindgen#environment-variables
LIBCLANG_PATH = pkgs.lib.makeLibraryPath [ pkgs.llvmPackages_latest.libclang.lib ];
}
}

View file

@ -16,9 +16,7 @@ impl GroupOpt {
GroupOpt::RemoveMembers(gcopt) => gcopt.copt.debug,
GroupOpt::SetMembers(gcopt) => gcopt.copt.debug,
GroupOpt::PurgeMembers(gcopt) => gcopt.copt.debug,
GroupOpt::SetDescription { copt, .. }
| GroupOpt::Rename { copt, .. }
| GroupOpt::SetMail { copt, .. } => copt.debug,
GroupOpt::Rename { copt, .. } | GroupOpt::SetMail { copt, .. } => copt.debug,
GroupOpt::Posix { commands } => match commands {
GroupPosix::Show(gcopt) => gcopt.copt.debug,
GroupPosix::Set(gcopt) => gcopt.copt.debug,
@ -180,26 +178,6 @@ impl GroupOpt {
Ok(_) => println!("Successfully set mail for group {}", name.as_str()),
}
}
GroupOpt::SetDescription {
copt,
name,
description,
} => {
let client = copt.to_client(OpType::Write).await;
let result = if let Some(description) = description {
client
.idm_group_set_description(name.as_str(), description.as_str())
.await
} else {
client.idm_group_purge_description(name.as_str()).await
};
match result {
Err(e) => handle_client_error(e, copt.output_mode),
Ok(_) => println!("Successfully set description for group {}", name.as_str()),
}
}
GroupOpt::Rename {
copt,
name,

View file

@ -348,14 +348,6 @@ pub enum GroupOpt {
name: String,
mail: Vec<String>,
},
/// Set the description of this group. If no description is provided, the value is cleared
#[clap(name = "set-description")]
SetDescription {
#[clap(flatten)]
copt: CommonOpt,
name: String,
description: Option<String>,
},
/// Set a new entry-managed-by for this group.
#[clap(name = "set-entry-manager")]
SetEntryManagedBy {

View file

@ -16,9 +16,6 @@ pub struct Config {
pub sync_password_as_unix_password: Option<bool>,
/// Maximum LDAP message size (in kilobytes)
pub max_ber_size: Option<usize>,
// pub entry: Option<Vec<EntryConfig>>,
#[serde(flatten)]
pub entry_map: BTreeMap<Uuid, EntryConfig>,

View file

@ -306,7 +306,6 @@ async fn run_sync(
// Preflight check.
// * can we connect to ipa?
let mut ipa_client = match LdapClientBuilder::new(&sync_config.ipa_uri)
.max_ber_size(sync_config.max_ber_size)
.add_tls_ca(&sync_config.ipa_ca)
.build()
.await

View file

@ -59,14 +59,6 @@ fn group_attr_gidnumber() -> String {
Attribute::GidNumber.to_string()
}
#[derive(Debug, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum GroupAttrSchema {
Rfc2307,
#[default]
Rfc2307Bis,
}
#[derive(Debug, Deserialize)]
pub struct Config {
pub sync_token: String,
@ -110,14 +102,12 @@ pub struct Config {
pub group_attr_gidnumber: String,
#[serde(default = "group_attr_member")]
pub group_attr_member: String,
#[serde(default)]
pub group_attr_schema: GroupAttrSchema,
/// Maximum LDAP message size (in kilobytes)
pub max_ber_size: Option<usize>,
#[serde(flatten)]
pub entry_map: BTreeMap<Uuid, EntryConfig>,
/// Maximum LDAP message size (in kilobytes)
pub max_ber_size: Option<usize>,
}
#[derive(Debug, Deserialize, Default, Clone)]

View file

@ -11,26 +11,16 @@
// We allow expect since it forces good error messages at the least.
#![allow(clippy::expect_used)]
use crate::config::{Config, EntryConfig, GroupAttrSchema};
mod config;
mod error;
use crate::config::{Config, EntryConfig};
use crate::error::SyncError;
use chrono::Utc;
use clap::Parser;
use cron::Schedule;
use kanidm_client::KanidmClientBuilder;
use kanidm_lib_file_permissions::readonly as file_permissions_readonly;
use kanidm_proto::constants::ATTR_OBJECTCLASS;
use kanidm_proto::scim_v1::{
MultiValueAttr, ScimEntry, ScimSshPubKey, ScimSyncGroup, ScimSyncPerson, ScimSyncRequest,
ScimSyncRetentionMode, ScimSyncState,
};
#[cfg(target_family = "unix")]
use kanidm_utils_users::{get_current_gid, get_current_uid, get_effective_gid, get_effective_uid};
use kanidmd_lib::prelude::Attribute;
use ldap3_client::{
proto::{self, LdapFilter},
LdapClient, LdapClientBuilder, LdapSyncRepl, LdapSyncReplEntry, LdapSyncStateValue,
};
use std::collections::{BTreeMap, BTreeSet};
use std::fs::metadata;
use std::fs::File;
use std::io::Read;
@ -48,12 +38,22 @@ use tokio::net::TcpListener;
use tokio::runtime;
use tokio::sync::broadcast;
use tokio::time::sleep;
use tracing::{debug, error, info, warn};
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter};
mod config;
mod error;
use kanidm_client::KanidmClientBuilder;
use kanidm_lib_file_permissions::readonly as file_permissions_readonly;
use kanidm_proto::scim_v1::{
MultiValueAttr, ScimEntry, ScimSshPubKey, ScimSyncGroup, ScimSyncPerson, ScimSyncRequest,
ScimSyncRetentionMode, ScimSyncState,
};
#[cfg(target_family = "unix")]
use kanidm_utils_users::{get_current_gid, get_current_uid, get_effective_gid, get_effective_uid};
use ldap3_client::{proto, LdapClientBuilder, LdapSyncRepl, LdapSyncReplEntry, LdapSyncStateValue};
include!("./opt.rs");
@ -343,7 +343,7 @@ async fn run_sync(
LdapSyncRepl::Success {
cookie,
refresh_deletes: _,
mut entries,
entries,
delete_uuids,
present_uuids,
} => {
@ -393,14 +393,6 @@ async fn run_sync(
}
};
if matches!(sync_config.group_attr_schema, GroupAttrSchema::Rfc2307) {
// Since the schema is rfc 2307, this means that the names of members
// in any group are uids, not dn's, so we need to resolve these now.
resolve_member_uid_to_dn(&mut ldap_client, &mut entries, sync_config)
.await
.map_err(|_| SyncError::Preprocess)?;
};
let entries = match process_ldap_sync_result(entries, sync_config).await {
Ok(ssr) => ssr,
Err(()) => {
@ -452,99 +444,6 @@ async fn run_sync(
// done!
}
async fn resolve_member_uid_to_dn(
ldap_client: &mut LdapClient,
ldap_entries: &mut [LdapSyncReplEntry],
sync_config: &Config,
) -> Result<(), ()> {
let mut lookup_cache: BTreeMap<String, String> = Default::default();
for sync_entry in ldap_entries.iter_mut() {
let oc = sync_entry
.entry
.attrs
.get(ATTR_OBJECTCLASS)
.ok_or_else(|| {
error!("Invalid entry - no object class {}", sync_entry.entry.dn);
})?;
if !oc.contains(&sync_config.group_objectclass) {
// Not a group, skip.
continue;
}
// It's a group, does it have memberUid? We pop this out here
// because we plan to replace it.
let members = sync_entry
.entry
.remove_ava(&sync_config.group_attr_member)
.unwrap_or_default();
// Now, search all the members to dns.
let mut resolved_members: BTreeSet<String> = Default::default();
for member_uid in members {
if let Some(member_dn) = lookup_cache.get(&member_uid) {
resolved_members.insert(member_dn.to_string());
} else {
// Not in cache, search it. We use a syncrepl request here as this
// can bypass some query limits. Note we set the sync cookie to None.
let filter = LdapFilter::And(vec![
// Always put uid first as openldap can't query optimise.
LdapFilter::Equality(
sync_config.person_attr_user_name.clone(),
member_uid.clone(),
),
LdapFilter::Equality(
ATTR_OBJECTCLASS.into(),
sync_config.person_objectclass.clone(),
),
]);
let mode = proto::SyncRequestMode::RefreshOnly;
let sync_result = ldap_client
.syncrepl(sync_config.ldap_sync_base_dn.clone(), filter, None, mode)
.await
.map_err(|err| {
debug!(?member_uid, ?sync_entry.entry_uuid);
error!(
?err,
"Failed to perform syncrepl to resolve members from ldap"
);
})?;
// Get the memberDN out now.
let member_dn = match sync_result {
LdapSyncRepl::Success { mut entries, .. } => {
let Some(resolved_entry) = entries.pop() else {
warn!(?member_uid, "Unable to resolve member, no matching entries");
continue;
};
resolved_entry.entry.dn.clone()
}
_ => {
error!("Invalid sync repl result state");
return Err(());
}
};
// cache it.
lookup_cache.insert(member_uid, member_dn.clone());
resolved_members.insert(member_dn);
}
}
// Put the members back in resolved as DN's now.
sync_entry
.entry
.attrs
.insert(sync_config.group_attr_member.clone(), resolved_members);
}
Ok(())
}
async fn process_ldap_sync_result(
ldap_entries: Vec<LdapSyncReplEntry>,
sync_config: &Config,

View file

@ -1,19 +0,0 @@
current_dir = $(shell pwd)
dev_install:
@ echo "WARNING: THIS WILL BREAK EXISTING UNIXD INSTALLS"
@ echo "ctrl-c now if this is not what you want"
@ read
@ echo "LAST CHANCE"
@ sleep 5
ln -s -f $(current_dir)/../platform/opensuse/kanidm-unixd.service /etc/systemd/system/kanidm-unixd.service
ln -s -f $(current_dir)/../platform/opensuse/kanidm-unixd-tasks.service /etc/systemd/system/kanidm-unixd-tasks.service
ln -s -f $(current_dir)/../target/debug/kanidm-unix /usr/sbin/kanidm-unix
ln -s -f $(current_dir)/../target/debug/kanidm_ssh_authorizedkeys /usr/sbin/kanidm_ssh_authorizedkeys
ln -s -f $(current_dir)/../target/debug/kanidm_unixd_tasks /usr/sbin/kanidm_unixd_tasks
ln -s -f $(current_dir)/../target/debug/kanidm_unixd /usr/sbin/kanidm_unixd
ln -s -f $(current_dir)/../target/debug/libpam_kanidm.so /lib64/security/pam_kanidm.so
ln -s -f $(current_dir)/../target/debug/libnss_kanidm.so /usr/lib64/libnss_kanidm.so.2

View file

@ -7,13 +7,6 @@ use std::io::Read;
use std::path::Path;
use std::str::FromStr;
#[derive(Serialize, Deserialize, Debug)]
pub struct EtcDb {
pub users: Vec<EtcUser>,
pub shadow: Vec<EtcShadow>,
pub groups: Vec<EtcGroup>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct EtcUser {
pub name: String,
@ -46,7 +39,7 @@ pub fn read_etc_passwd_file<P: AsRef<Path>>(path: P) -> Result<Vec<EtcUser>, Uni
parse_etc_passwd(contents.as_slice()).map_err(|_| UnixIntegrationError)
}
#[derive(PartialEq, Default)]
#[derive(Debug, PartialEq, Default)]
pub enum CryptPw {
Sha256(String),
Sha512(String),
@ -63,16 +56,6 @@ impl fmt::Display for CryptPw {
}
}
impl fmt::Debug for CryptPw {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CryptPw::Invalid => write!(f, "x"),
CryptPw::Sha256(_s) => write!(f, "crypt sha256"),
CryptPw::Sha512(_s) => write!(f, "crypt sha512"),
}
}
}
impl FromStr for CryptPw {
type Err = &'static str;

View file

@ -1,4 +1,4 @@
use crate::unix_passwd::{EtcDb, EtcGroup, EtcUser};
use crate::unix_passwd::{EtcGroup, EtcUser};
use kanidm_proto::internal::OperationError;
use serde::{Deserialize, Serialize};
@ -200,12 +200,6 @@ pub struct HomeDirectoryInfo {
pub aliases: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TaskRequestFrame {
pub id: u64,
pub req: TaskRequest,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum TaskRequest {
HomeDirectory(HomeDirectoryInfo),
@ -213,9 +207,8 @@ pub enum TaskRequest {
#[derive(Serialize, Deserialize, Debug)]
pub enum TaskResponse {
Success(u64),
Success,
Error(String),
NotifyShadowChange(EtcDb),
}
#[test]

Some files were not shown because too many files have changed in this diff Show more