mirror of
https://github.com/kanidm/kanidm.git
synced 2025-06-01 05:43:55 +02:00
Compare commits
15 commits
ef0fe92099
...
0f04e4bd50
Author | SHA1 | Date | |
---|---|---|---|
|
0f04e4bd50 | ||
|
ce410f440c | ||
|
77271c1720 | ||
|
e838da9a08 | ||
|
94b7285cbb | ||
|
af6f55b1fe | ||
|
211e7d4e89 | ||
|
ccde675cd2 | ||
|
b96fe49b99 | ||
|
c89f0c011e | ||
|
b15ff89b39 | ||
|
1f5ce2617d | ||
|
f68906bf1b | ||
|
7a9bb9eac2 | ||
|
0ce1bbeddc |
.github/workflows
Cargo.lockCargo.tomlREADME.mdRELEASE_NOTES.mdbook/src
examples
libs
crypto
file_permissions
profiles
sketching
users
platform/freebsd/client
proto
pykanidm
server
tools
cli
device_flow
iam_migrations
orca
unix_integration
common
nss_kanidm
pam_kanidm
resolver
17
.github/workflows/docker_build_kanidm.yml
vendored
17
.github/workflows/docker_build_kanidm.yml
vendored
|
@ -35,9 +35,15 @@ jobs:
|
|||
needs:
|
||||
- set_tag_values
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
- name: Build kanidm
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
|
@ -47,6 +53,9 @@ jobs:
|
|||
build-args: |
|
||||
"KANIDM_FEATURES="
|
||||
file: tools/Dockerfile
|
||||
context: .
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
# Must use OCI exporter for multi-arch: https://github.com/docker/buildx/pull/1813
|
||||
outputs: type=oci,dest=/tmp/kanidm-docker.tar
|
||||
- name: Upload artifact
|
||||
|
@ -60,8 +69,8 @@ jobs:
|
|||
# This step is split so that we don't apply "packages: write" permission
|
||||
# except when uploading the final Docker image to GHCR.
|
||||
runs-on: ubuntu-latest
|
||||
if: ( github.ref_type == 'tag' || github.ref == 'refs/heads/master' ) && github.repository == 'kanidm/kanidm'
|
||||
needs: kanidm_build
|
||||
if: ( github.ref_type == 'tag' || github.ref == 'refs/heads/master' )
|
||||
needs: [kanidm_build, set_tag_values]
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
|
@ -78,4 +87,4 @@ jobs:
|
|||
echo "${{ secrets.GITHUB_TOKEN }}" | \
|
||||
oras login -u "${{ github.actor }}" --password-stdin ghcr.io
|
||||
oras copy --from-oci-layout "/tmp/kanidm-docker.tar:devel" \
|
||||
"ghcr.io/${{ github.repository_owner }}/kanidm:devel"
|
||||
"ghcr.io/${{ needs.set_tag_values.outputs.owner_lc }}/kanidm:devel"
|
29
.github/workflows/docker_build_kanidmd.yml
vendored
29
.github/workflows/docker_build_kanidmd.yml
vendored
|
@ -35,27 +35,15 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
needs: set_tag_values
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
# list of Docker images to use as base name for tags
|
||||
# images: |
|
||||
# kanidm/kanidmd
|
||||
# ghcr.io/username/app
|
||||
# generate Docker tags based on the following events/attributes
|
||||
tags: |
|
||||
type=schedule
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=sha
|
||||
|
||||
- name: Build kanidmd
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
|
@ -64,6 +52,9 @@ jobs:
|
|||
# build-args: |
|
||||
# "KANIDM_BUILD_OPTIONS=-j1"
|
||||
file: server/Dockerfile
|
||||
context: .
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
# Must use OCI exporter for multi-arch: https://github.com/docker/buildx/pull/1813
|
||||
outputs: type=oci,dest=/tmp/kanidmd-docker.tar
|
||||
- name: Upload artifact
|
||||
|
@ -77,8 +68,8 @@ jobs:
|
|||
# This step is split so that we don't apply "packages: write" permission
|
||||
# except when uploading the final Docker image to GHCR.
|
||||
runs-on: ubuntu-latest
|
||||
if: ( github.ref_type== 'tag' || github.ref == 'refs/heads/master' ) && github.repository == 'kanidm/kanidm'
|
||||
needs: kanidmd_build
|
||||
if: ( github.ref_type== 'tag' || github.ref == 'refs/heads/master' )
|
||||
needs: [kanidmd_build, set_tag_values]
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
|
@ -95,4 +86,4 @@ jobs:
|
|||
echo "${{ secrets.GITHUB_TOKEN }}" | \
|
||||
oras login -u "${{ github.actor }}" --password-stdin ghcr.io
|
||||
oras copy --from-oci-layout "/tmp/kanidmd-docker.tar:devel" \
|
||||
"ghcr.io/${{ github.repository_owner }}/kanidmd:devel"
|
||||
"ghcr.io/${{ needs.set_tag_values.outputs.owner_lc }}/kanidmd:devel"
|
17
.github/workflows/docker_build_radiusd.yml
vendored
17
.github/workflows/docker_build_radiusd.yml
vendored
|
@ -35,17 +35,26 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
needs: set_tag_values
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
- name: Build radius
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/arm64,linux/amd64
|
||||
tags: ghcr.io/${{ needs.set_tag_values.outputs.owner_lc }}/radius:devel,ghcr.io/${{ needs.set_tag_values.outputs.owner_lc }}/radius:${{ needs.set_tag_values.outputs.ref_name}}
|
||||
file: rlm_python/Dockerfile
|
||||
context: .
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
# Must use OCI exporter for multi-arch: https://github.com/docker/buildx/pull/1813
|
||||
outputs: type=oci,dest=/tmp/radius-docker.tar
|
||||
- name: Upload artifact
|
||||
|
@ -59,8 +68,8 @@ jobs:
|
|||
# This step is split so that we don't apply "packages: write" permission
|
||||
# except when uploading the final Docker image to GHCR.
|
||||
runs-on: ubuntu-latest
|
||||
if: ( github.ref_type == 'tag' || github.ref == 'refs/heads/master' ) && github.repository == 'kanidm/kanidm'
|
||||
needs: radius_build
|
||||
if: ( github.ref_type == 'tag' || github.ref == 'refs/heads/master' )
|
||||
needs: [radius_build, set_tag_values]
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
|
@ -79,4 +88,4 @@ jobs:
|
|||
echo "${{ secrets.GITHUB_TOKEN }}" | \
|
||||
oras login -u "${{ github.actor }}" --password-stdin ghcr.io
|
||||
oras copy --from-oci-layout "/tmp/radius-docker.tar:devel" \
|
||||
"ghcr.io/${{ github.repository_owner }}/radius:devel"
|
||||
"ghcr.io/${{ needs.set_tag_values.outputs.owner_lc }}/radius:devel"
|
432
Cargo.lock
generated
432
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
39
Cargo.toml
39
Cargo.toml
|
@ -1,10 +1,10 @@
|
|||
[workspace.package]
|
||||
version = "1.5.0-dev"
|
||||
version = "1.6.0-dev"
|
||||
authors = [
|
||||
"William Brown <william@blackhats.net.au>",
|
||||
"James Hodgkinson <james@terminaloutcomes.com>",
|
||||
]
|
||||
rust-version = "1.79"
|
||||
rust-version = "1.80"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
homepage = "https://github.com/kanidm/kanidm/"
|
||||
|
@ -120,21 +120,23 @@ codegen-units = 256
|
|||
|
||||
# kanidm-hsm-crypto = { path = "../hsm-crypto" }
|
||||
|
||||
libnss = { git = "https://github.com/Firstyear/libnss-rs.git", branch = "20250207-freebsd" }
|
||||
|
||||
[workspace.dependencies]
|
||||
kanidmd_core = { path = "./server/core", version = "=1.5.0-dev" }
|
||||
kanidmd_lib = { path = "./server/lib", version = "=1.5.0-dev" }
|
||||
kanidmd_lib_macros = { path = "./server/lib-macros", version = "=1.5.0-dev" }
|
||||
kanidmd_testkit = { path = "./server/testkit", version = "=1.5.0-dev" }
|
||||
kanidm_build_profiles = { path = "./libs/profiles", version = "=1.5.0-dev" }
|
||||
kanidm_client = { path = "./libs/client", version = "=1.5.0-dev" }
|
||||
kanidmd_core = { path = "./server/core", version = "=1.6.0-dev" }
|
||||
kanidmd_lib = { path = "./server/lib", version = "=1.6.0-dev" }
|
||||
kanidmd_lib_macros = { path = "./server/lib-macros", version = "=1.6.0-dev" }
|
||||
kanidmd_testkit = { path = "./server/testkit", version = "=1.6.0-dev" }
|
||||
kanidm_build_profiles = { path = "./libs/profiles", version = "=1.6.0-dev" }
|
||||
kanidm_client = { path = "./libs/client", version = "=1.6.0-dev" }
|
||||
kanidm-hsm-crypto = "^0.2.0"
|
||||
kanidm_lib_crypto = { path = "./libs/crypto", version = "=1.5.0-dev" }
|
||||
kanidm_lib_file_permissions = { path = "./libs/file_permissions", version = "=1.5.0-dev" }
|
||||
kanidm_proto = { path = "./proto", version = "=1.5.0-dev" }
|
||||
kanidm_unix_common = { path = "./unix_integration/common", version = "=1.5.0-dev" }
|
||||
kanidm_utils_users = { path = "./libs/users", version = "=1.5.0-dev" }
|
||||
scim_proto = { path = "./libs/scim_proto", version = "=1.5.0-dev" }
|
||||
sketching = { path = "./libs/sketching", version = "=1.5.0-dev" }
|
||||
kanidm_lib_crypto = { path = "./libs/crypto", version = "=1.6.0-dev" }
|
||||
kanidm_lib_file_permissions = { path = "./libs/file_permissions", version = "=1.6.0-dev" }
|
||||
kanidm_proto = { path = "./proto", version = "=1.6.0-dev" }
|
||||
kanidm_unix_common = { path = "./unix_integration/common", version = "=1.6.0-dev" }
|
||||
kanidm_utils_users = { path = "./libs/users", version = "=1.6.0-dev" }
|
||||
scim_proto = { path = "./libs/scim_proto", version = "=1.6.0-dev" }
|
||||
sketching = { path = "./libs/sketching", version = "=1.6.0-dev" }
|
||||
|
||||
anyhow = { version = "1.0.95" }
|
||||
argon2 = { version = "0.5.3", features = ["alloc"] }
|
||||
|
@ -163,15 +165,15 @@ clap_complete = "^4.5.42"
|
|||
chrono = "^0.4.39"
|
||||
compact_jwt = { version = "^0.4.2", default-features = false }
|
||||
concread = "^0.5.3"
|
||||
cron = "0.12.1"
|
||||
cron = "0.15.0"
|
||||
crossbeam = "0.8.4"
|
||||
csv = "1.3.1"
|
||||
dialoguer = "0.10.4"
|
||||
dialoguer = "0.11.0"
|
||||
dhat = "0.3.3"
|
||||
dyn-clone = "^1.0.17"
|
||||
fernet = "^0.2.1"
|
||||
filetime = "^0.2.24"
|
||||
fs4 = "^0.8.3"
|
||||
fs4 = "^0.12.0"
|
||||
futures = "^0.3.31"
|
||||
futures-util = { version = "^0.3.30", features = ["sink"] }
|
||||
gix = { version = "0.64.0", default-features = false }
|
||||
|
@ -223,7 +225,6 @@ opentelemetry-semantic-conventions = "0.27.0"
|
|||
tracing-opentelemetry = "0.28.0"
|
||||
tracing-core = "0.1.33"
|
||||
|
||||
paste = "^1.0.14"
|
||||
peg = "0.8"
|
||||
pkg-config = "^0.3.31"
|
||||
prctl = "1.0.0"
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
# Kanidm - Simple and Secure Identity Management
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/kanidm/kanidm/master/artwork/logo-small.png" width="20%" height="auto" />
|
||||
</p>
|
||||

|
||||
|
||||
## About
|
||||
|
||||
|
|
157
RELEASE_NOTES.md
157
RELEASE_NOTES.md
|
@ -1,42 +1,93 @@
|
|||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/kanidm/kanidm/master/artwork/logo-small.png" width="20%" height="auto" />
|
||||
</p>
|
||||
# Kanidm Release Notes
|
||||
|
||||
# Getting Started
|
||||

|
||||
|
||||
## Getting Started
|
||||
|
||||
To get started, see the [kanidm book]
|
||||
|
||||
# Feedback
|
||||
## Feedback
|
||||
|
||||
We value your feedback! First, please see our [code of conduct]. If you have questions please join
|
||||
our [gitter community channel] so that we can help. If you find a bug or issue, we'd love you to
|
||||
report it to our [issue tracker].
|
||||
|
||||
# Release Notes
|
||||
## Release Notes
|
||||
|
||||
## 2024-11-01 - Kanidm 1.4.0
|
||||
### 2025-02-09 - Kanidm 1.5.0
|
||||
|
||||
This is the latest stable release of the Kanidm Identity Management project. Every release is the
|
||||
combined effort of our community and we appreciate their invaluable contributions, comments,
|
||||
questions, feedback and support.
|
||||
|
||||
You should review our
|
||||
[support documentation](https://github.com/kanidm/kanidm/blob/master/book/src/support.md) as this
|
||||
[support documentation] as this
|
||||
may have important effects on your distribution or upgrades in future.
|
||||
|
||||
Before upgrading you should review
|
||||
[our upgrade documentation](https://github.com/kanidm/kanidm/blob/master/book/src/server_updates.md#general-update-notes)
|
||||
[our upgrade documentation]
|
||||
|
||||
### 1.4.0 Important Changes
|
||||
#### 1.5.0 Important Changes
|
||||
|
||||
- There has been a lot of tweaks to how cookies are handled in this release, if you're having issues with the login flow please clear all cookies as an initial troubleshooting step.
|
||||
|
||||
#### 1.5.0 Release Highlights
|
||||
|
||||
- Many updates to the UI!
|
||||
- SSH Keys in Credentials Update (#3027)
|
||||
- Improved error message when PassKey is missing PIN (mainly for Firefox) (#3403)
|
||||
- Fix the password reset form and possible resolver issue (#3398)
|
||||
- Fixed unrecoverable error page doesn't include logo or domain name (#3352)
|
||||
- Add support for prefers-color-scheme using Bootstrap classes. Dark mode! (#3327)
|
||||
- Automatically trigger passkeys on login view (#3307)
|
||||
- Two new operating systems!
|
||||
- Initial OpenBSD support (#3381)
|
||||
- FreeBSD client (#3333)
|
||||
- Many SCIM-related improvements
|
||||
- SCIM access control (#3359)
|
||||
- SCIM put (#3151)
|
||||
- OAuth2 Things
|
||||
- Allow OAuth2 with empty `state` parameter (#3396)
|
||||
- Allow POST on oauth userinfo (#3395)
|
||||
- Add OAuth2 `response_mode=fragment` (#3335)
|
||||
- Add CORS headers to jwks and userinfo (#3283)
|
||||
- Allowing SPN query with non-SPN structured data in LDAP (#3400)
|
||||
- Correctly return that uuid2spn changed on domain rename (#3402)
|
||||
- RADIUS startup fixing (#3388)
|
||||
- Repaired systemd reload notifications (#3355)
|
||||
- Add `ssh_publickeys` as a claim for OAuth2 (#3346)
|
||||
- Allow modification of password minimum length (#3345)
|
||||
- PAM on Debian, enable use_first_pass by default (#3326)
|
||||
- Allow opt-in of easter eggs (#3308)
|
||||
- Allow reseting account policy values to defaults (#3306)
|
||||
- Ignore system users for UPG synthesiseation (#3297)
|
||||
- Allow group managers to modify entry-managed-by (#3272)
|
||||
|
||||
And many more!
|
||||
|
||||
### 2024-11-01 - Kanidm 1.4.0
|
||||
|
||||
This is the latest stable release of the Kanidm Identity Management project. Every release is the
|
||||
combined effort of our community and we appreciate their invaluable contributions, comments,
|
||||
questions, feedback and support.
|
||||
|
||||
You should review our
|
||||
[support documentation] as this
|
||||
may have important effects on your distribution or upgrades in future.
|
||||
|
||||
Before upgrading you should review
|
||||
[our upgrade documentation]
|
||||
|
||||
#### 1.4.0 Important Changes
|
||||
|
||||
- The web user interface has been rewritten and now supports theming. You will notice that your
|
||||
domain displayname is included in a number of locations on upgrade, and that you can set
|
||||
your own domain and OAuth2 client icons.
|
||||
- OAuth2 strict redirect uri is now required. Ensure you have read
|
||||
[our upgrade documentation](https://github.com/kanidm/kanidm/blob/master/book/src/server_updates.md#general-update-notes).
|
||||
[our upgrade documentation].
|
||||
and taken the needed steps before upgrading.
|
||||
|
||||
### 1.4.0 Release Highlights
|
||||
#### 1.4.0 Release Highlights
|
||||
|
||||
- Improve handling of client timeouts when the server is under high load
|
||||
- Resolve a minor issue preventing some credential updates from saving
|
||||
|
@ -65,20 +116,20 @@ and taken the needed steps before upgrading.
|
|||
- Rewrite the entire web frontend to be simpler and faster, allowing more features to be added
|
||||
in the future. Greatly improves user experience as the pages are now very fast to load!
|
||||
|
||||
## 2024-08-07 - Kanidm 1.3.0
|
||||
### 2024-08-07 - Kanidm 1.3.0
|
||||
|
||||
This is the latest stable release of the Kanidm Identity Management project. Every release is the
|
||||
combined effort of our community and we appreciate their invaluable contributions, comments,
|
||||
questions, feedback and support.
|
||||
|
||||
You should review our
|
||||
[support documentation](https://github.com/kanidm/kanidm/blob/master/book/src/support.md) as this
|
||||
[support documentation] as this
|
||||
may have important effects on your distribution or upgrades in future.
|
||||
|
||||
Before upgrading you should review
|
||||
[our upgrade documentation](https://github.com/kanidm/kanidm/blob/master/book/src/server_updates.md#general-update-notes)
|
||||
[our upgrade documentation]
|
||||
|
||||
### 1.3.0 Important Changes
|
||||
#### 1.3.0 Important Changes
|
||||
|
||||
- New GID number constraints are now enforced in this version. To upgrade from 1.2.0 all accounts
|
||||
and groups must adhere to these rules. See [our upgrade documentation]. about tools to help you
|
||||
|
@ -89,7 +140,7 @@ Before upgrading you should review
|
|||
by PassKeys which give a better user experience.
|
||||
- Kanidm now supports FreeBSD and Illumos in addition to Linux
|
||||
|
||||
### 1.3.0 Release Highlights
|
||||
#### 1.3.0 Release Highlights
|
||||
|
||||
- TOTP update user interface improvements
|
||||
- Improved error messages when a load balancer is failing
|
||||
|
@ -112,24 +163,24 @@ Before upgrading you should review
|
|||
- Strict redirect URI enforcement in OAuth2
|
||||
- Substring indexing for improved search performance
|
||||
|
||||
## 2024-05-01 - Kanidm 1.2.0
|
||||
### 2024-05-01 - Kanidm 1.2.0
|
||||
|
||||
This is the first stable release of the Kanidm Identity Management project. We want to thank every
|
||||
one in our community who has supported to the project to this point with their invaluable
|
||||
contributions, comments, questions, feedback and support.
|
||||
|
||||
Importantly this release makes a number of changes to our project's support processes. You should
|
||||
review our [support documentation](https://github.com/kanidm/kanidm/blob/master/book/src/support.md)
|
||||
review our [support documentation]
|
||||
as this may have important effects on your distribution or upgrades in future.
|
||||
|
||||
### 1.2.0 Important Changes
|
||||
#### 1.2.0 Important Changes
|
||||
|
||||
- On upgrade all OAuth2 sessions and user sessions will be reset due to changes in cryptographic key
|
||||
handling. This does not affect api tokens.
|
||||
- There is a maximum limit of 48 interactive sessions for persons where older sessions are
|
||||
automatically removed.
|
||||
|
||||
### 1.2.0 Release Highlights
|
||||
#### 1.2.0 Release Highlights
|
||||
|
||||
- The book now contains a list of supported RFCs and standards
|
||||
- Add code challenge methods to OIDC discovery
|
||||
|
@ -154,7 +205,7 @@ as this may have important effects on your distribution or upgrades in future.
|
|||
- Migrate cryptographic key handling to an object model with future HSM support
|
||||
- Limit maximum active sessions on an account to 48
|
||||
|
||||
## 2024-02-07 - Kanidm 1.1.0-rc.16
|
||||
### 2024-02-07 - Kanidm 1.1.0-rc.16
|
||||
|
||||
This is the sixteenth pre-release of the Kanidm Identity Management project. Pre-releases are to
|
||||
help get feedback and ideas from the community on how we can continue to make this project better.
|
||||
|
@ -163,7 +214,7 @@ This is the final release candidate before we publish a release version. We beli
|
|||
server interfaces are stable and reliable enough for people to depend on, and to develop external
|
||||
tools to interact with Kanidm.
|
||||
|
||||
### 1.1.0-rc.16 Release Highlights
|
||||
#### 1.1.0-rc.16 Release Highlights
|
||||
|
||||
- Replication for two node environments is now supported
|
||||
- Account policy supports password minimum length
|
||||
|
@ -182,7 +233,7 @@ tools to interact with Kanidm.
|
|||
- Support RFC6749 Client Credentials Grant
|
||||
- Support custom claim maps in OIDC
|
||||
|
||||
## 2023-10-31 - Kanidm 1.1.0-beta14
|
||||
### 2023-10-31 - Kanidm 1.1.0-beta14
|
||||
|
||||
This is the fourteenth pre-release of the Kanidm Identity Management project. Pre-releases are to
|
||||
help get feedback and ideas from the community on how we can continue to make this project better.
|
||||
|
@ -191,7 +242,7 @@ At this point we believe we are on the final stretch to making something we cons
|
|||
ready". After this we will start to ship release candidates as our focus will now be changing to
|
||||
finish our production components and the stability of the API's for longer term support.
|
||||
|
||||
### 1.1.0-beta14 Release Highlights
|
||||
#### 1.1.0-beta14 Release Highlights
|
||||
|
||||
- Replication is in Beta! Please test carefully!
|
||||
- Web UI WASM has been split up, significantly improving the responsiveness.
|
||||
|
@ -205,7 +256,7 @@ finish our production components and the stability of the API's for longer term
|
|||
- Removed a lot of uses of `unwrap` and `expect` to improve reliability.
|
||||
- Account policy framework is now in place.
|
||||
|
||||
## 2023-05-01 - Kanidm 1.1.0-beta13
|
||||
### 2023-05-01 - Kanidm 1.1.0-beta13
|
||||
|
||||
This is the thirteenth pre-release of the Kanidm Identity Management project. Pre-releases are to
|
||||
help get feedback and ideas from the community on how we can continue to make this project better.
|
||||
|
@ -214,7 +265,7 @@ At this point we believe we are on the final stretch to making something we cons
|
|||
ready". After this we will start to ship release candidates as our focus will now be changing to
|
||||
finish our production components and the stability of the API's for longer term support.
|
||||
|
||||
### 1.1.0-beta13 Release Highlights
|
||||
#### 1.1.0-beta13 Release Highlights
|
||||
|
||||
- Replication foundations
|
||||
- Full implementation of replication refresh
|
||||
|
@ -255,7 +306,7 @@ finish our production components and the stability of the API's for longer term
|
|||
- Improve create-reset-token user experience
|
||||
- Improve self-healing for some reference issues
|
||||
|
||||
## 2023-05-01 - Kanidm 1.1.0-alpha12
|
||||
### 2023-05-01 - Kanidm 1.1.0-alpha12
|
||||
|
||||
This is the twelfth alpha series release of the Kanidm Identity Management project. Alpha releases
|
||||
are to help get feedback and ideas from the community on how we can continue to make this project
|
||||
|
@ -266,7 +317,7 @@ done so yet is we haven't decided if we want to commit to the current API layout
|
|||
There are still things we want to change there. Otherwise the server is stable and reliable for
|
||||
production usage.
|
||||
|
||||
### Release Highlights
|
||||
#### 1.1.0-alpha12 Release Highlights
|
||||
|
||||
- Allow full server content replication in testing (yes we're finally working on replication!)
|
||||
- Improve OAuth2 to allow scoped members to see RS they can access for UI flows
|
||||
|
@ -286,7 +337,7 @@ production usage.
|
|||
- Add exclusive process lock to daemon
|
||||
- Allow dns/rdns in ldap search contexts
|
||||
|
||||
## 2023-02-01 - Kanidm 1.1.0-alpha11
|
||||
### 2023-02-01 - Kanidm 1.1.0-alpha11
|
||||
|
||||
This is the eleventh alpha series release of the Kanidm Identity Management project. Alpha releases
|
||||
are to help get feedback and ideas from the community on how we can continue to make this project
|
||||
|
@ -296,7 +347,7 @@ The project is shaping up very nicely, and a beta will be coming soon! The main
|
|||
done so yet is we haven't decided if we want to commit to the current API layout and freeze it yet.
|
||||
There are still things we want to change there. Otherwise the server is stable and reliable.
|
||||
|
||||
### Release Highlights
|
||||
#### 1.1.0-alpha11 Release Highlights
|
||||
|
||||
- Support /etc/skel home dir templates in kanidm-unixd
|
||||
- Improve warning messages for openssl when a cryptographic routine is not supported
|
||||
|
@ -317,7 +368,7 @@ There are still things we want to change there. Otherwise the server is stable a
|
|||
- Improve the access control module to evaluate access in a clearer way
|
||||
- Allow synced users to correct modify their local sessions
|
||||
|
||||
## 2022-11-01 - Kanidm 1.1.0-alpha10
|
||||
### 2022-11-01 - Kanidm 1.1.0-alpha10
|
||||
|
||||
This is the tenth alpha series release of the Kanidm Identity Management project. Alpha releases are
|
||||
to help get feedback and ideas from the community on how we can continue to make this project better
|
||||
|
@ -325,12 +376,12 @@ for a future supported release.
|
|||
|
||||
The project is shaping up very nicely, and a beta will be coming soon!
|
||||
|
||||
### Upgrade Note
|
||||
#### 1.1.0-alpha10 Upgrade Note
|
||||
|
||||
This version will _require_ TLS on all servers, even if behind a load balancer or TLS terminating
|
||||
proxy. You should be ready for this change when you upgrade to the latest version.
|
||||
|
||||
### Release Highlights
|
||||
#### 1.1.0-alpha10 Release Highlights
|
||||
|
||||
- Management and tracking of authenticated sessions
|
||||
- Make upgrade migrations more robust when upgrading over multiple versions
|
||||
|
@ -352,7 +403,7 @@ proxy. You should be ready for this change when you upgrade to the latest versio
|
|||
- Cleanup of expired authentication sessions
|
||||
- Improved administration of password badlists
|
||||
|
||||
## 2022-08-02 - Kanidm 1.1.0-alpha9
|
||||
### 2022-08-02 - Kanidm 1.1.0-alpha9
|
||||
|
||||
This is the ninth alpha series release of the Kanidm Identity Management project. Alpha releases are
|
||||
to help get feedback and ideas from the community on how we can continue to make this project better
|
||||
|
@ -360,7 +411,7 @@ for a future supported release.
|
|||
|
||||
The project is shaping up very nicely, and a beta will be coming soon!
|
||||
|
||||
### Release Highlights
|
||||
#### 1.1.0-alpha9 Release Highlights
|
||||
|
||||
- Inclusion of a Python3 API library
|
||||
- Improve orca usability
|
||||
|
@ -376,13 +427,13 @@ The project is shaping up very nicely, and a beta will be coming soon!
|
|||
- CTAP2+ support in Webauthn via CLI
|
||||
- Radius supports EAP TLS identities in addition to EAP PEAP
|
||||
|
||||
## 2022-05-01 - Kanidm 1.1.0-alpha8
|
||||
### 2022-05-01 - Kanidm 1.1.0-alpha8
|
||||
|
||||
This is the eighth alpha series release of the Kanidm Identity Management project. Alpha releases
|
||||
are to help get feedback and ideas from the community on how we can continue to make this project
|
||||
better for a future supported release.
|
||||
|
||||
### Release Highlights
|
||||
#### 1.1.0-alpha8 Release Highlights
|
||||
|
||||
- Foundations for cryptographic trusted device authentication
|
||||
- Foundations for new user onboarding and credential reset
|
||||
|
@ -398,13 +449,13 @@ better for a future supported release.
|
|||
- Highlight that the WebUI is in alpha to prevent confusion
|
||||
- Remove sync only client paths
|
||||
|
||||
## 2022-01-01 - Kanidm 1.1.0-alpha7
|
||||
### 2022-01-01 - Kanidm 1.1.0-alpha7
|
||||
|
||||
This is the seventh alpha series release of the Kanidm Identity Management project. Alpha releases
|
||||
are to help get feedback and ideas from the community on how we can continue to make this project
|
||||
better for a future supported release.
|
||||
|
||||
### Release Highlights
|
||||
#### 1.1.0-alpha7 Release Highlights
|
||||
|
||||
- OAuth2 scope to group mappings
|
||||
- Webauthn subdomain support
|
||||
|
@ -415,7 +466,7 @@ better for a future supported release.
|
|||
- Addition of email address attributes
|
||||
- Web UI improvements for OAuth2
|
||||
|
||||
## 2021-10-01 - Kanidm 1.1.0-alpha6
|
||||
### 2021-10-01 - Kanidm 1.1.0-alpha6
|
||||
|
||||
This is the sixth alpha series release of the Kanidm Identity Management project. Alpha releases are
|
||||
to help get feedback and ideas from the community on how we can continue to make this project better
|
||||
|
@ -424,7 +475,7 @@ for a future supported release.
|
|||
It's also a special release as Kanidm has just turned 3 years old! Thank you all for helping to
|
||||
bring the project this far! 🎉 🦀
|
||||
|
||||
### Release Highlights
|
||||
#### 1.1.0-alpha6 Release Highlights
|
||||
|
||||
- Support backup codes as MFA in case of lost TOTP/Webauthn
|
||||
- Dynamic menus on CLI for usernames when multiple sessions exist
|
||||
|
@ -444,13 +495,13 @@ bring the project this far! 🎉 🦀
|
|||
- Improvements to performance with high cache sizes
|
||||
- Session tokens persist over a session restart
|
||||
|
||||
## 2021-07-07 - Kanidm 1.1.0-alpha5
|
||||
### 2021-07-07 - Kanidm 1.1.0-alpha5
|
||||
|
||||
This is the fifth alpha series release of the Kanidm Identity Management project. Alpha releases are
|
||||
to help get feedback and ideas from the community on how we can continue to make this project better
|
||||
for a future supported release.
|
||||
|
||||
### Release Highlights
|
||||
#### 1.1.0-alpha5 Release Highlights
|
||||
|
||||
- Fix a major defect in how backup/restore worked
|
||||
- Improve query performance by caching partial queries
|
||||
|
@ -465,13 +516,13 @@ for a future supported release.
|
|||
- Statistical analysis of indexes to improve query optimisation
|
||||
- Handle broken TOTP authenticator apps
|
||||
|
||||
## 2021-04-01 - Kanidm 1.1.0-alpha4
|
||||
### 2021-04-01 - Kanidm 1.1.0-alpha4
|
||||
|
||||
This is the fourth alpha series release of the Kanidm Identity Management project. Alpha releases
|
||||
are to help get feedback and ideas from the community on how we can continue to make this project
|
||||
better for a future supported release.
|
||||
|
||||
### Release Highlights
|
||||
#### 1.1.0-alpha4 Release Highlights
|
||||
|
||||
- Performance Improvements
|
||||
- TOTP CLI enrollment
|
||||
|
@ -485,13 +536,13 @@ better for a future supported release.
|
|||
- Badlist checked at login to determine account compromise
|
||||
- Minor Fixes for attribute display
|
||||
|
||||
## 2021-01-01 - Kanidm 1.1.0-alpha3
|
||||
### 2021-01-01 - Kanidm 1.1.0-alpha3
|
||||
|
||||
This is the third alpha series release of the Kanidm Identity Management project. Alpha releases are
|
||||
to help get feedback and ideas from the community on how we can continue to make this project better
|
||||
for a future supported release.
|
||||
|
||||
### Release Highlights
|
||||
#### 1.1.0-alpha3 Release Highlights
|
||||
|
||||
- Account "valid from" and "expiry" times.
|
||||
- Rate limiting and softlocking of account credentials to prevent bruteforcing.
|
||||
|
@ -499,13 +550,13 @@ for a future supported release.
|
|||
- Rewrite of json authentication protocol components.
|
||||
- Unixd will cache "non-existent" items to improve nss/pam latency.
|
||||
|
||||
## 2020-10-01 - Kanidm 1.1.0-alpha2
|
||||
### 2020-10-01 - Kanidm 1.1.0-alpha2
|
||||
|
||||
This is the second alpha series release of the Kanidm Identity Management project. Alpha releases
|
||||
are to help get feedback and ideas from the community on how we can continue to make this project
|
||||
better for a future supported release.
|
||||
|
||||
### Release Highlights
|
||||
#### 1.1.0-alpha2 Release Highlights
|
||||
|
||||
- SIMD key lookups in container builds for datastructures
|
||||
- Server and Client hardening warnings for running users and file permissions
|
||||
|
@ -517,7 +568,7 @@ better for a future supported release.
|
|||
- Reduction in memory footprint during searches
|
||||
- Change authentication from cookies to auth-bearer tokens
|
||||
|
||||
## 2020-07-01 - Kanidm 1.1.0-alpha1
|
||||
### 2020-07-01 - Kanidm 1.1.0-alpha1
|
||||
|
||||
This is the first alpha series release of the Kanidm Identity Management project. Alpha releases are
|
||||
to help get feedback and ideas from the community on how we can continue to make this project better
|
||||
|
@ -536,7 +587,7 @@ people. I would especially like to thank:
|
|||
- Samuel Cabrero (scabrero)
|
||||
- Jim McDonough
|
||||
|
||||
### Release Highlights
|
||||
#### 1.1.0-alpha1 Release Highlights
|
||||
|
||||
- A working identity management server, including database
|
||||
- RADIUS authentication and docker images
|
||||
|
@ -552,3 +603,5 @@ people. I would especially like to thank:
|
|||
[gitter community channel]: https://gitter.im/kanidm/community
|
||||
[code of conduct]: https://github.com/kanidm/kanidm/blob/master/CODE_OF_CONDUCT.md
|
||||
[kanidm book]: https://kanidm.github.io/kanidm/stable/
|
||||
[our upgrade documentation]: https://github.com/kanidm/kanidm/blob/master/book/src/server_updates.md#general-update-notes
|
||||
[support documentation]: https://github.com/kanidm/kanidm/blob/master/book/src/support.md
|
||||
|
|
|
@ -3,57 +3,58 @@
|
|||
## Pre-Reqs
|
||||
|
||||
```bash
|
||||
cargo install cargo-audit
|
||||
cargo install cargo-outdated
|
||||
cargo install cargo-udeps
|
||||
cargo install cargo-machete
|
||||
cargo install --force \
|
||||
cargo-audit \
|
||||
cargo-outdated \
|
||||
cargo-udeps \
|
||||
cargo-machete
|
||||
```
|
||||
|
||||
## Pre Release Check List
|
||||
|
||||
### Start a release
|
||||
|
||||
- [ ] git checkout -b YYYYMMDD-pre-release
|
||||
- [ ] `git checkout -b "$(date +%Y%m%d)-pre-release"`
|
||||
|
||||
### Cargo Tasks
|
||||
|
||||
- [ ] Update MSRV if applicable
|
||||
- [ ] cargo update
|
||||
- [ ] `cargo update`
|
||||
- [ ] `RUSTC_BOOTSTRAP=1 cargo udeps`
|
||||
- [ ] `cargo machete`
|
||||
- [ ] cargo outdated -R
|
||||
- [ ] cargo audit
|
||||
- [ ] cargo test
|
||||
- [ ] `cargo machete --with-metadata`
|
||||
- [ ] `cargo outdated -R`
|
||||
- [ ] `cargo audit`
|
||||
- [ ] `cargo test`
|
||||
|
||||
- [ ] setup a local instance and run orca (TBD)
|
||||
- [ ] store a copy an an example db (TBD)
|
||||
|
||||
### Code Changes
|
||||
|
||||
- [ ] upgrade crypto policy values if required
|
||||
- [ ] upgrade crypto policy values if required (see `libs/crypto/src/lib.rs` -> `CryptoPolicy`)
|
||||
- [ ] check for breaking db entry changes.
|
||||
|
||||
### Administration
|
||||
|
||||
- [ ] Update `RELEASE_NOTES.md`
|
||||
- [ ] Update `README.md`
|
||||
- [ ] cargo test
|
||||
- [ ] git commit -a -m "Release Notes"
|
||||
- [ ] git push origin YYYYMMDD-pre-release
|
||||
- [ ] `cargo test`
|
||||
- [ ] `git commit -a -m 'chore: Release Notes'`
|
||||
- [ ] `git push origin "$(date +%Y%m%d)-pre-release"`
|
||||
- [ ] Merge PR
|
||||
|
||||
### Git Management
|
||||
|
||||
- [ ] git checkout master
|
||||
- [ ] git pull
|
||||
- [ ] `git checkout master`
|
||||
- [ ] `git pull`
|
||||
- [ ] git checkout -b 1.x.0 (Note no v to prevent ref conflict)
|
||||
- [ ] update version to set pre tag in ./Cargo.toml
|
||||
- [ ] git commit -m "Release 1.x.0-pre"
|
||||
- [ ] git tag v1.x.0-pre
|
||||
- [ ] `git commit -m "Release $(cargo metadata --format-version 1 | jq '.packages[] | select(.name=="kanidm_proto") | .version')-pre"`
|
||||
- [ ] `git tag v$(cargo metadata --format-version 1 | jq '.packages[] | select(.name=="kanidm_proto") | .version')-pre`
|
||||
|
||||
- [ ] Final inspect of the branch
|
||||
|
||||
- [ ] git push origin 1.x.0 --tags
|
||||
- [ ] `git push origin "$(cargo metadata --format-version 1 | jq '.packages[] | select(.name=="kanidm_proto") | .version')" --tags`
|
||||
|
||||
- [ ] github -> Ensure release branch is protected
|
||||
|
||||
|
@ -106,4 +107,3 @@ cargo install cargo-machete
|
|||
### Distro
|
||||
|
||||
- [ ] vendor and release to build.opensuse.org
|
||||
|
||||
|
|
|
@ -145,7 +145,8 @@ with a dn of `dn=token` and provide the api token in the password.
|
|||
> [!NOTE]
|
||||
>
|
||||
> The `dn=token` keyword is guaranteed to not be used by any other entry, which is why it was chosen
|
||||
> as the keyword to initiate api token binds.
|
||||
> as the keyword to initiate api token binds. Additionally it is not required, leaving the field empty
|
||||
> will fall back to the service-account if a "password" is provided
|
||||
|
||||
```bash
|
||||
ldapwhoami -H ldaps://URL -x -D "dn=token" -w "TOKEN"
|
||||
|
@ -234,6 +235,7 @@ ldapwhoami ... -x -D '22a65b6c-80c8-4e1a-9b76-3f3afdff8400'
|
|||
ldapwhoami ... -x -D 'spn=test1@idm.example.com,dc=idm,dc=example,dc=com'
|
||||
ldapwhoami ... -x -D 'name=test1,dc=idm,dc=example,dc=com'
|
||||
```
|
||||
<sub>in fact, the key of the bind isn't used at all so `googoogaaga=test1` is entirely valid</sub> ;)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
|
@ -70,6 +70,31 @@ anything special for Kanidm (or another provider).
|
|||
**Note:** some apps automatically append `/.well-known/openid-configuration` to
|
||||
the end of an OIDC Discovery URL, so you may need to omit that.
|
||||
|
||||
|
||||
<dl>
|
||||
<dt>[Webfinger](https://datatracker.ietf.org/doc/html/rfc7033) URL</dt>
|
||||
|
||||
<dd>
|
||||
|
||||
`https://idm.example.com/oauth2/openid/:client_id:/.well-known/webfinger`
|
||||
|
||||
The webfinger URL is implemented for each OpenID client, under its specific endpoint, giving full control to the administrator regarding which to use.
|
||||
|
||||
To make this compliant with the standard, it must be made available under the correct [well-known endpoint](https://datatracker.ietf.org/doc/html/rfc7033#section-10.1) (e.g `example.com/.well-known/webfinger`), typically via a reverse proxy or similar. Kanidm doesn't currently provide a mechanism for this URI rewrite.
|
||||
|
||||
One example would be dedicating one client as the "primary" or "default" and redirecting all requests to that. Alternatively, source IP or other request metadata could be used to decide which client to forward the request to.
|
||||
|
||||
### Caddy
|
||||
`Caddyfile`
|
||||
```caddy
|
||||
# assuming a kanidm service with domain "example.com"
|
||||
example.com {
|
||||
redir /.well-known/webfinger https://idm.example.com/oauth2/openid/:client_id:{uri} 307
|
||||
}
|
||||
```
|
||||
**Note:** the `{uri}` is important as it preserves the original request past the redirect.
|
||||
|
||||
|
||||
</dd>
|
||||
|
||||
<dt>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Kanidm minimal Service Configuration - /etc/kanidm/config
|
||||
# For a full example and documentation, see /usr/share/kanidm/kanidm
|
||||
# For a full example and documentation, see /usr/share/kanidm/config
|
||||
# or `example/kanidm` in the source repository.
|
||||
|
||||
# Replace this with your kanidmd URI and uncomment the line
|
||||
|
|
|
@ -35,3 +35,7 @@ x509-cert = { workspace = true, features = ["pem"] }
|
|||
|
||||
[dev-dependencies]
|
||||
sketching = { workspace = true }
|
||||
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["openssl-sys"]
|
||||
|
|
|
@ -16,8 +16,5 @@ doctest = false
|
|||
|
||||
[dependencies]
|
||||
|
||||
[target.'cfg(target_family = "windows")'.dependencies]
|
||||
whoami = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_family = "windows"))'.dependencies]
|
||||
kanidm_utils_users = { workspace = true }
|
||||
|
|
|
@ -28,3 +28,7 @@ toml = { workspace = true }
|
|||
[build-dependencies]
|
||||
base64 = { workspace = true }
|
||||
gix = { workspace = true, default-features = false }
|
||||
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["gix"]
|
||||
|
|
|
@ -3,5 +3,6 @@
|
|||
server_admin_bind_path = "/data/kanidmd.sock"
|
||||
server_ui_pkg_path = "/hpkg"
|
||||
server_config_path = "/data/server.toml"
|
||||
client_config_path = "/data/config"
|
||||
resolver_config_path = "/data/unixd"
|
||||
resolver_unix_shell_path = "/bin/false"
|
||||
|
|
|
@ -3,5 +3,6 @@ cpu_flags = "native"
|
|||
server_admin_bind_path = "/tmp/kanidmd.sock"
|
||||
server_ui_pkg_path = "../core/static"
|
||||
server_config_path = "../../examples/insecure_server.toml"
|
||||
client_config_path = "/etc/kanidm/config"
|
||||
resolver_config_path = "/tmp/unixd"
|
||||
resolver_unix_shell_path = "/bin/bash"
|
||||
|
|
|
@ -3,5 +3,6 @@
|
|||
server_admin_bind_path = "/var/run/kanidmd/sock"
|
||||
server_ui_pkg_path = "/usr/local/share/kanidm/ui/hpkg"
|
||||
server_config_path = "/usr/local/etc/kanidm/server.toml"
|
||||
client_config_path = "/usr/local/etc/kanidm/config"
|
||||
resolver_config_path = "/usr/local/etc/kanidm/unixd"
|
||||
resolver_unix_shell_path = "/bin/sh"
|
||||
|
|
|
@ -3,5 +3,6 @@
|
|||
server_admin_bind_path = "/var/run/kanidmd/sock"
|
||||
server_ui_pkg_path = "/usr/share/kanidm/ui/hpkg"
|
||||
server_config_path = "/etc/kanidm/server.toml"
|
||||
client_config_path = "/etc/kanidm/config"
|
||||
resolver_config_path = "/etc/kanidm/unixd"
|
||||
resolver_unix_shell_path = "/bin/bash"
|
||||
|
|
|
@ -59,6 +59,7 @@ struct ProfileConfig {
|
|||
server_admin_bind_path: String,
|
||||
server_config_path: String,
|
||||
server_ui_pkg_path: String,
|
||||
client_config_path: String,
|
||||
resolver_config_path: String,
|
||||
resolver_unix_shell_path: String,
|
||||
}
|
||||
|
@ -139,6 +140,10 @@ pub fn apply_profile() {
|
|||
"cargo:rustc-env=KANIDM_SERVER_CONFIG_PATH={}",
|
||||
profile_cfg.server_config_path
|
||||
);
|
||||
println!(
|
||||
"cargo:rustc-env=KANIDM_CLIENT_CONFIG_PATH={}",
|
||||
profile_cfg.client_config_path
|
||||
);
|
||||
println!(
|
||||
"cargo:rustc-env=KANIDM_RESOLVER_CONFIG_PATH={}",
|
||||
profile_cfg.resolver_config_path
|
||||
|
|
|
@ -17,7 +17,6 @@ test = false
|
|||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
gethostname = "0.5.0"
|
||||
num_enum = { workspace = true }
|
||||
opentelemetry = { workspace = true, features = ["metrics"] }
|
||||
opentelemetry-otlp = { workspace = true, default-features = false, features = [
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
[package]
|
||||
name = "kanidm_utils_users"
|
||||
description = "Kanidm utility crate"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
version = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
|
||||
[lib]
|
||||
test = true
|
||||
|
|
|
@ -1,624 +0,0 @@
|
|||
CARGO_CRATES= addr2line-0.24.2 \
|
||||
adler2-2.0.0 \
|
||||
ahash-0.8.11 \
|
||||
aho-corasick-1.1.3 \
|
||||
allocator-api2-0.2.21 \
|
||||
android-tzdata-0.1.1 \
|
||||
android_system_properties-0.1.5 \
|
||||
anstream-0.6.18 \
|
||||
anstyle-1.0.10 \
|
||||
anstyle-parse-0.2.6 \
|
||||
anstyle-query-1.1.2 \
|
||||
anstyle-wincon-3.0.6 \
|
||||
anyhow-1.0.95 \
|
||||
arc-swap-1.7.1 \
|
||||
argon2-0.5.3 \
|
||||
askama-0.12.1 \
|
||||
askama_axum-0.4.0 \
|
||||
askama_derive-0.12.5 \
|
||||
askama_escape-0.10.3 \
|
||||
askama_parser-0.2.1 \
|
||||
asn1-rs-0.6.2 \
|
||||
asn1-rs-derive-0.5.1 \
|
||||
asn1-rs-impl-0.2.0 \
|
||||
assert_cmd-2.0.16 \
|
||||
async-compression-0.4.18 \
|
||||
async-stream-0.3.6 \
|
||||
async-stream-impl-0.3.6 \
|
||||
async-trait-0.1.83 \
|
||||
atomic-waker-1.1.2 \
|
||||
authenticator-0.4.1 \
|
||||
autocfg-1.4.0 \
|
||||
axum-0.6.20 \
|
||||
axum-0.7.9 \
|
||||
axum-core-0.3.4 \
|
||||
axum-core-0.4.5 \
|
||||
axum-extra-0.9.6 \
|
||||
axum-htmx-0.5.0 \
|
||||
axum-macros-0.4.2 \
|
||||
axum-server-0.7.1 \
|
||||
backtrace-0.3.74 \
|
||||
base32-0.5.1 \
|
||||
base64-0.13.1 \
|
||||
base64-0.21.7 \
|
||||
base64-0.22.1 \
|
||||
base64ct-1.6.0 \
|
||||
base64urlsafedata-0.5.1 \
|
||||
basic-toml-0.1.9 \
|
||||
bindgen-0.66.1 \
|
||||
bindgen-0.70.1 \
|
||||
bit-set-0.5.3 \
|
||||
bit-set-0.8.0 \
|
||||
bit-vec-0.6.3 \
|
||||
bit-vec-0.8.0 \
|
||||
bitfield-0.13.2 \
|
||||
bitflags-1.3.2 \
|
||||
bitflags-2.6.0 \
|
||||
blake2-0.10.6 \
|
||||
block-buffer-0.10.4 \
|
||||
borrow-or-share-0.2.2 \
|
||||
bstr-1.11.1 \
|
||||
bumpalo-3.16.0 \
|
||||
bytecount-0.6.8 \
|
||||
bytemuck-1.21.0 \
|
||||
byteorder-1.5.0 \
|
||||
bytes-1.9.0 \
|
||||
cc-1.2.5 \
|
||||
cexpr-0.6.0 \
|
||||
cfg-if-1.0.0 \
|
||||
cfg_aliases-0.2.1 \
|
||||
checked_int_cast-1.0.0 \
|
||||
chrono-0.4.39 \
|
||||
clang-sys-1.8.1 \
|
||||
clap-4.5.23 \
|
||||
clap_builder-4.5.23 \
|
||||
clap_complete-4.5.40 \
|
||||
clap_derive-4.5.18 \
|
||||
clap_lex-0.7.4 \
|
||||
clru-0.6.2 \
|
||||
color_quant-1.1.0 \
|
||||
colorchoice-1.0.3 \
|
||||
compact_jwt-0.4.3 \
|
||||
concread-0.5.3 \
|
||||
console-0.15.10 \
|
||||
const-oid-0.9.6 \
|
||||
cookie-0.16.2 \
|
||||
cookie-0.18.1 \
|
||||
cookie_store-0.21.1 \
|
||||
core-foundation-0.9.4 \
|
||||
core-foundation-0.10.0 \
|
||||
core-foundation-sys-0.8.7 \
|
||||
cpufeatures-0.2.16 \
|
||||
crc32fast-1.4.2 \
|
||||
cron-0.12.1 \
|
||||
crossbeam-0.8.4 \
|
||||
crossbeam-channel-0.5.14 \
|
||||
crossbeam-deque-0.8.6 \
|
||||
crossbeam-epoch-0.9.18 \
|
||||
crossbeam-queue-0.3.12 \
|
||||
crossbeam-utils-0.8.21 \
|
||||
crypto-common-0.1.6 \
|
||||
csv-1.3.1 \
|
||||
csv-core-0.1.11 \
|
||||
darling-0.14.4 \
|
||||
darling-0.20.10 \
|
||||
darling_core-0.14.4 \
|
||||
darling_core-0.20.10 \
|
||||
darling_macro-0.14.4 \
|
||||
darling_macro-0.20.10 \
|
||||
data-encoding-2.6.0 \
|
||||
der-0.7.9 \
|
||||
der-parser-9.0.0 \
|
||||
der_derive-0.7.3 \
|
||||
deranged-0.3.11 \
|
||||
derive_builder-0.12.0 \
|
||||
derive_builder_core-0.12.0 \
|
||||
derive_builder_macro-0.12.0 \
|
||||
devd-rs-0.3.6 \
|
||||
dhat-0.3.3 \
|
||||
dialoguer-0.10.4 \
|
||||
difflib-0.4.0 \
|
||||
digest-0.10.7 \
|
||||
dirs-4.0.0 \
|
||||
dirs-sys-0.3.7 \
|
||||
displaydoc-0.2.5 \
|
||||
doc-comment-0.3.3 \
|
||||
document-features-0.2.10 \
|
||||
dunce-1.0.5 \
|
||||
dyn-clone-1.0.17 \
|
||||
either-1.13.0 \
|
||||
email_address-0.2.9 \
|
||||
encode_unicode-1.0.0 \
|
||||
encoding_rs-0.8.35 \
|
||||
enum-iterator-2.1.0 \
|
||||
enum-iterator-derive-1.4.0 \
|
||||
enumflags2-0.7.10 \
|
||||
enumflags2_derive-0.7.10 \
|
||||
equivalent-1.0.1 \
|
||||
errno-0.3.10 \
|
||||
escargot-0.5.13 \
|
||||
fallible-iterator-0.2.0 \
|
||||
fallible-streaming-iterator-0.1.9 \
|
||||
fancy-regex-0.11.0 \
|
||||
fancy-regex-0.14.0 \
|
||||
fantoccini-0.21.3 \
|
||||
faster-hex-0.9.0 \
|
||||
fastrand-2.3.0 \
|
||||
fernet-0.2.2 \
|
||||
file-id-0.1.0 \
|
||||
filetime-0.2.25 \
|
||||
fixedbitset-0.4.2 \
|
||||
flagset-0.4.6 \
|
||||
flate2-1.0.35 \
|
||||
fluent-uri-0.3.2 \
|
||||
fnv-1.0.7 \
|
||||
foldhash-0.1.4 \
|
||||
foreign-types-0.3.2 \
|
||||
foreign-types-shared-0.1.1 \
|
||||
form_urlencoded-1.2.1 \
|
||||
fraction-0.15.3 \
|
||||
fs4-0.8.4 \
|
||||
fsevent-sys-4.1.0 \
|
||||
futures-0.3.31 \
|
||||
futures-channel-0.3.31 \
|
||||
futures-core-0.3.31 \
|
||||
futures-executor-0.3.31 \
|
||||
futures-io-0.3.31 \
|
||||
futures-macro-0.3.31 \
|
||||
futures-sink-0.3.31 \
|
||||
futures-task-0.3.31 \
|
||||
futures-util-0.3.31 \
|
||||
generic-array-0.14.7 \
|
||||
gethostname-0.5.0 \
|
||||
getrandom-0.2.15 \
|
||||
gif-0.13.1 \
|
||||
gimli-0.31.1 \
|
||||
gix-0.64.0 \
|
||||
gix-actor-0.31.5 \
|
||||
gix-chunk-0.4.10 \
|
||||
gix-commitgraph-0.24.3 \
|
||||
gix-config-0.38.0 \
|
||||
gix-config-value-0.14.10 \
|
||||
gix-date-0.8.7 \
|
||||
gix-diff-0.44.1 \
|
||||
gix-discover-0.33.0 \
|
||||
gix-features-0.38.2 \
|
||||
gix-fs-0.11.3 \
|
||||
gix-glob-0.16.5 \
|
||||
gix-hash-0.14.2 \
|
||||
gix-hashtable-0.5.2 \
|
||||
gix-lock-14.0.0 \
|
||||
gix-macros-0.1.5 \
|
||||
gix-object-0.42.3 \
|
||||
gix-odb-0.61.1 \
|
||||
gix-pack-0.51.1 \
|
||||
gix-path-0.10.13 \
|
||||
gix-quote-0.4.14 \
|
||||
gix-ref-0.45.0 \
|
||||
gix-refspec-0.23.1 \
|
||||
gix-revision-0.27.2 \
|
||||
gix-revwalk-0.13.2 \
|
||||
gix-sec-0.10.10 \
|
||||
gix-tempfile-14.0.2 \
|
||||
gix-trace-0.1.11 \
|
||||
gix-traverse-0.39.2 \
|
||||
gix-url-0.27.5 \
|
||||
gix-utils-0.1.13 \
|
||||
gix-validate-0.8.5 \
|
||||
glob-0.3.1 \
|
||||
h2-0.3.26 \
|
||||
h2-0.4.7 \
|
||||
half-1.8.3 \
|
||||
hashbrown-0.12.3 \
|
||||
hashbrown-0.14.5 \
|
||||
hashbrown-0.15.2 \
|
||||
hashlink-0.8.4 \
|
||||
heck-0.5.0 \
|
||||
hex-0.4.3 \
|
||||
home-0.5.11 \
|
||||
hostname-validator-1.1.1 \
|
||||
http-0.2.12 \
|
||||
http-1.2.0 \
|
||||
http-body-0.4.6 \
|
||||
http-body-1.0.1 \
|
||||
http-body-util-0.1.2 \
|
||||
http-range-header-0.4.2 \
|
||||
httparse-1.9.5 \
|
||||
httpdate-1.0.3 \
|
||||
humansize-2.1.3 \
|
||||
hyper-0.14.32 \
|
||||
hyper-1.5.2 \
|
||||
hyper-rustls-0.24.2 \
|
||||
hyper-rustls-0.27.5 \
|
||||
hyper-timeout-0.4.1 \
|
||||
hyper-tls-0.6.0 \
|
||||
hyper-util-0.1.10 \
|
||||
iana-time-zone-0.1.61 \
|
||||
iana-time-zone-haiku-0.1.2 \
|
||||
icu_collections-1.5.0 \
|
||||
icu_locid-1.5.0 \
|
||||
icu_locid_transform-1.5.0 \
|
||||
icu_locid_transform_data-1.5.0 \
|
||||
icu_normalizer-1.5.0 \
|
||||
icu_normalizer_data-1.5.0 \
|
||||
icu_properties-1.5.1 \
|
||||
icu_properties_data-1.5.0 \
|
||||
icu_provider-1.5.0 \
|
||||
icu_provider_macros-1.5.0 \
|
||||
ident_case-1.0.1 \
|
||||
idlset-0.2.5 \
|
||||
idna-1.0.3 \
|
||||
idna_adapter-1.2.0 \
|
||||
image-0.23.14 \
|
||||
image-0.24.9 \
|
||||
indexmap-1.9.3 \
|
||||
indexmap-2.7.0 \
|
||||
inotify-0.9.6 \
|
||||
inotify-sys-0.1.5 \
|
||||
ipnet-2.10.1 \
|
||||
is_terminal_polyfill-1.70.1 \
|
||||
itertools-0.10.5 \
|
||||
itertools-0.13.0 \
|
||||
itoa-1.0.14 \
|
||||
jpeg-decoder-0.3.1 \
|
||||
js-sys-0.3.76 \
|
||||
jsonschema-0.28.0 \
|
||||
kanidm-hsm-crypto-0.2.0 \
|
||||
kqueue-1.0.8 \
|
||||
kqueue-sys-1.0.4 \
|
||||
lazy_static-1.5.0 \
|
||||
lazycell-1.3.0 \
|
||||
lber-0.4.2 \
|
||||
ldap3_client-0.5.2 \
|
||||
ldap3_proto-0.5.2 \
|
||||
libc-0.2.169 \
|
||||
libloading-0.8.6 \
|
||||
libm-0.2.11 \
|
||||
libmimalloc-sys-0.1.39 \
|
||||
libnss-0.8.0 \
|
||||
libredox-0.1.3 \
|
||||
libsqlite3-sys-0.25.2 \
|
||||
libudev-0.2.0 \
|
||||
libudev-sys-0.1.4 \
|
||||
linux-raw-sys-0.4.14 \
|
||||
litemap-0.7.4 \
|
||||
litrs-0.4.1 \
|
||||
lock_api-0.4.12 \
|
||||
lodepng-3.10.7 \
|
||||
log-0.4.22 \
|
||||
lru-0.12.5 \
|
||||
malloced-1.3.1 \
|
||||
matchers-0.1.0 \
|
||||
matchit-0.7.3 \
|
||||
mathru-0.13.0 \
|
||||
memchr-2.7.4 \
|
||||
memmap2-0.9.5 \
|
||||
memoffset-0.8.0 \
|
||||
mimalloc-0.1.43 \
|
||||
mime-0.3.17 \
|
||||
mime_guess-2.0.5 \
|
||||
minimal-lexical-0.2.1 \
|
||||
miniz_oxide-0.8.2 \
|
||||
mintex-0.1.3 \
|
||||
mio-0.8.11 \
|
||||
mio-1.0.3 \
|
||||
multer-3.1.0 \
|
||||
native-tls-0.2.12 \
|
||||
nix-0.29.0 \
|
||||
nom-7.1.3 \
|
||||
nonempty-0.8.1 \
|
||||
notify-6.1.1 \
|
||||
notify-debouncer-full-0.1.0 \
|
||||
nu-ansi-term-0.46.0 \
|
||||
num-0.4.3 \
|
||||
num-bigint-0.4.6 \
|
||||
num-cmp-0.1.0 \
|
||||
num-complex-0.4.6 \
|
||||
num-conv-0.1.0 \
|
||||
num-derive-0.3.3 \
|
||||
num-integer-0.1.46 \
|
||||
num-iter-0.1.45 \
|
||||
num-rational-0.3.2 \
|
||||
num-rational-0.4.2 \
|
||||
num-traits-0.2.19 \
|
||||
num_enum-0.5.11 \
|
||||
num_enum_derive-0.5.11 \
|
||||
num_threads-0.1.7 \
|
||||
oauth2-4.4.2 \
|
||||
object-0.36.5 \
|
||||
oid-0.2.1 \
|
||||
oid-registry-0.7.1 \
|
||||
once_cell-1.20.2 \
|
||||
openssl-0.10.68 \
|
||||
openssl-macros-0.1.1 \
|
||||
openssl-probe-0.1.5 \
|
||||
openssl-sys-0.9.104 \
|
||||
opentelemetry-0.20.0 \
|
||||
opentelemetry-http-0.9.0 \
|
||||
opentelemetry-otlp-0.13.0 \
|
||||
opentelemetry-proto-0.3.0 \
|
||||
opentelemetry-semantic-conventions-0.12.0 \
|
||||
opentelemetry_api-0.20.0 \
|
||||
opentelemetry_sdk-0.20.0 \
|
||||
ordered-float-3.9.2 \
|
||||
outref-0.5.1 \
|
||||
overload-0.1.1 \
|
||||
parking_lot-0.12.3 \
|
||||
parking_lot_core-0.9.10 \
|
||||
password-hash-0.5.0 \
|
||||
paste-1.0.15 \
|
||||
peeking_take_while-0.1.2 \
|
||||
peg-0.8.4 \
|
||||
peg-macros-0.8.4 \
|
||||
peg-runtime-0.8.3 \
|
||||
pem-rfc7468-0.7.0 \
|
||||
percent-encoding-2.3.1 \
|
||||
petgraph-0.6.5 \
|
||||
picky-asn1-0.8.0 \
|
||||
picky-asn1-der-0.4.1 \
|
||||
picky-asn1-x509-0.12.0 \
|
||||
pin-project-1.1.7 \
|
||||
pin-project-internal-1.1.7 \
|
||||
pin-project-lite-0.2.15 \
|
||||
pin-utils-0.1.0 \
|
||||
pkg-config-0.3.31 \
|
||||
powerfmt-0.2.0 \
|
||||
ppv-lite86-0.2.20 \
|
||||
prctl-1.0.0 \
|
||||
predicates-3.1.3 \
|
||||
predicates-core-1.0.9 \
|
||||
predicates-tree-1.0.12 \
|
||||
prettyplease-0.2.25 \
|
||||
proc-macro-crate-1.3.1 \
|
||||
proc-macro-error-1.0.4 \
|
||||
proc-macro-error-attr-1.0.4 \
|
||||
proc-macro2-1.0.92 \
|
||||
prodash-28.0.0 \
|
||||
prost-0.11.9 \
|
||||
prost-derive-0.11.9 \
|
||||
psl-types-2.0.11 \
|
||||
publicsuffix-2.3.0 \
|
||||
qrcode-0.12.0 \
|
||||
quick-error-2.0.1 \
|
||||
quinn-0.11.6 \
|
||||
quinn-proto-0.11.9 \
|
||||
quinn-udp-0.5.9 \
|
||||
quote-1.0.38 \
|
||||
rand-0.8.5 \
|
||||
rand_chacha-0.3.1 \
|
||||
rand_core-0.6.4 \
|
||||
redox_syscall-0.5.8 \
|
||||
redox_users-0.4.6 \
|
||||
ref-cast-1.0.23 \
|
||||
ref-cast-impl-1.0.23 \
|
||||
reference-counted-singleton-0.1.5 \
|
||||
referencing-0.28.0 \
|
||||
regex-1.11.1 \
|
||||
regex-automata-0.1.10 \
|
||||
regex-automata-0.4.9 \
|
||||
regex-syntax-0.6.29 \
|
||||
regex-syntax-0.8.5 \
|
||||
reqwest-0.11.27 \
|
||||
reqwest-0.12.11 \
|
||||
rgb-0.8.50 \
|
||||
ring-0.17.8 \
|
||||
rpassword-5.0.1 \
|
||||
runloop-0.1.0 \
|
||||
rusqlite-0.28.0 \
|
||||
rust-embed-8.5.0 \
|
||||
rust-embed-impl-8.5.0 \
|
||||
rust-embed-utils-8.5.0 \
|
||||
rustc-demangle-0.1.24 \
|
||||
rustc-hash-1.1.0 \
|
||||
rustc-hash-2.1.0 \
|
||||
rusticata-macros-4.1.0 \
|
||||
rustix-0.38.42 \
|
||||
rustls-0.21.12 \
|
||||
rustls-0.23.20 \
|
||||
rustls-native-certs-0.8.1 \
|
||||
rustls-pemfile-1.0.4 \
|
||||
rustls-pemfile-2.2.0 \
|
||||
rustls-pki-types-1.10.1 \
|
||||
rustls-webpki-0.101.7 \
|
||||
rustls-webpki-0.102.8 \
|
||||
rustversion-1.0.18 \
|
||||
ryu-1.0.18 \
|
||||
same-file-1.0.6 \
|
||||
schannel-0.1.27 \
|
||||
scopeguard-1.2.0 \
|
||||
sct-0.7.1 \
|
||||
sd-notify-0.4.3 \
|
||||
security-framework-2.11.1 \
|
||||
security-framework-3.1.0 \
|
||||
security-framework-sys-2.13.0 \
|
||||
selinux-0.4.6 \
|
||||
selinux-sys-0.6.13 \
|
||||
semver-1.0.24 \
|
||||
serde-1.0.217 \
|
||||
serde_bytes-0.11.15 \
|
||||
serde_cbor-0.11.2 \
|
||||
serde_cbor_2-0.12.0-dev \
|
||||
serde_derive-1.0.217 \
|
||||
serde_json-1.0.134 \
|
||||
serde_path_to_error-0.1.16 \
|
||||
serde_urlencoded-0.7.1 \
|
||||
serde_with-3.12.0 \
|
||||
serde_with_macros-3.12.0 \
|
||||
sha-crypt-0.5.0 \
|
||||
sha1_smol-1.0.1 \
|
||||
sha2-0.10.8 \
|
||||
sharded-slab-0.1.7 \
|
||||
shell-words-1.1.0 \
|
||||
shellexpand-2.1.2 \
|
||||
shlex-1.3.0 \
|
||||
signal-hook-registry-1.4.2 \
|
||||
slab-0.4.9 \
|
||||
smallvec-1.13.2 \
|
||||
smartstring-1.0.1 \
|
||||
smolset-1.3.1 \
|
||||
socket2-0.5.8 \
|
||||
spin-0.9.8 \
|
||||
spki-0.7.3 \
|
||||
sptr-0.3.2 \
|
||||
sshkey-attest-0.5.0 \
|
||||
sshkeys-0.3.3 \
|
||||
stable_deref_trait-1.2.0 \
|
||||
static_assertions-1.1.0 \
|
||||
strsim-0.10.0 \
|
||||
strsim-0.11.1 \
|
||||
subtle-2.6.1 \
|
||||
svg-0.13.1 \
|
||||
syn-1.0.109 \
|
||||
syn-2.0.93 \
|
||||
sync_wrapper-0.1.2 \
|
||||
sync_wrapper-1.0.2 \
|
||||
synstructure-0.13.1 \
|
||||
system-configuration-0.5.1 \
|
||||
system-configuration-sys-0.5.0 \
|
||||
target-lexicon-0.12.16 \
|
||||
tempfile-3.14.0 \
|
||||
termtree-0.5.1 \
|
||||
thiserror-1.0.69 \
|
||||
thiserror-2.0.8 \
|
||||
thiserror-impl-1.0.69 \
|
||||
thiserror-impl-2.0.8 \
|
||||
thousands-0.2.0 \
|
||||
thread_local-1.1.8 \
|
||||
time-0.3.37 \
|
||||
time-core-0.1.2 \
|
||||
time-macros-0.2.19 \
|
||||
tinystr-0.7.6 \
|
||||
tinyvec-1.8.1 \
|
||||
tinyvec_macros-0.1.1 \
|
||||
tls_codec-0.4.1 \
|
||||
tls_codec_derive-0.4.1 \
|
||||
tokio-1.42.0 \
|
||||
tokio-io-timeout-1.2.0 \
|
||||
tokio-macros-2.4.0 \
|
||||
tokio-native-tls-0.3.1 \
|
||||
tokio-openssl-0.6.5 \
|
||||
tokio-rustls-0.24.1 \
|
||||
tokio-rustls-0.26.1 \
|
||||
tokio-stream-0.1.17 \
|
||||
tokio-util-0.7.13 \
|
||||
toml-0.5.11 \
|
||||
toml_datetime-0.6.8 \
|
||||
toml_edit-0.19.15 \
|
||||
tonic-0.9.2 \
|
||||
tower-0.4.13 \
|
||||
tower-0.5.2 \
|
||||
tower-http-0.6.2 \
|
||||
tower-layer-0.3.3 \
|
||||
tower-service-0.3.3 \
|
||||
tracing-0.1.41 \
|
||||
tracing-attributes-0.1.28 \
|
||||
tracing-core-0.1.33 \
|
||||
tracing-forest-0.1.6 \
|
||||
tracing-log-0.1.4 \
|
||||
tracing-log-0.2.0 \
|
||||
tracing-opentelemetry-0.21.0 \
|
||||
tracing-subscriber-0.3.19 \
|
||||
try-lock-0.2.5 \
|
||||
tss-esapi-8.0.0-alpha \
|
||||
tss-esapi-sys-0.5.0 \
|
||||
typenum-1.17.0 \
|
||||
unicase-2.8.0 \
|
||||
unicode-bom-2.0.3 \
|
||||
unicode-ident-1.0.14 \
|
||||
unicode-normalization-0.1.24 \
|
||||
unicode-segmentation-1.12.0 \
|
||||
unicode-width-0.2.0 \
|
||||
untrusted-0.9.0 \
|
||||
url-2.5.4 \
|
||||
urlencoding-2.1.3 \
|
||||
utf16_iter-1.0.5 \
|
||||
utf8_iter-1.0.4 \
|
||||
utf8parse-0.2.2 \
|
||||
utoipa-4.2.3 \
|
||||
utoipa-gen-4.3.1 \
|
||||
utoipa-swagger-ui-6.0.0 \
|
||||
uuid-1.11.0 \
|
||||
uuid-simd-0.8.0 \
|
||||
valuable-0.1.0 \
|
||||
vcpkg-0.2.15 \
|
||||
version_check-0.9.5 \
|
||||
vsimd-0.8.0 \
|
||||
wait-timeout-0.2.0 \
|
||||
walkdir-2.5.0 \
|
||||
want-0.3.1 \
|
||||
wasi-0.11.0+wasi-snapshot-preview1 \
|
||||
wasite-0.1.0 \
|
||||
wasm-bindgen-0.2.99 \
|
||||
wasm-bindgen-backend-0.2.99 \
|
||||
wasm-bindgen-futures-0.4.49 \
|
||||
wasm-bindgen-macro-0.2.99 \
|
||||
wasm-bindgen-macro-support-0.2.99 \
|
||||
wasm-bindgen-shared-0.2.99 \
|
||||
web-sys-0.3.76 \
|
||||
web-time-1.1.0 \
|
||||
webauthn-attestation-ca-0.5.1 \
|
||||
webauthn-authenticator-rs-0.5.1 \
|
||||
webauthn-rs-0.5.1 \
|
||||
webauthn-rs-core-0.5.1 \
|
||||
webauthn-rs-proto-0.5.1 \
|
||||
webdriver-0.50.0 \
|
||||
webpki-roots-0.25.4 \
|
||||
webpki-roots-0.26.7 \
|
||||
weezl-0.1.8 \
|
||||
which-4.4.2 \
|
||||
whoami-1.5.2 \
|
||||
winapi-0.3.9 \
|
||||
winapi-i686-pc-windows-gnu-0.4.0 \
|
||||
winapi-util-0.1.9 \
|
||||
winapi-x86_64-pc-windows-gnu-0.4.0 \
|
||||
windows-0.41.0 \
|
||||
windows-core-0.52.0 \
|
||||
windows-registry-0.2.0 \
|
||||
windows-result-0.2.0 \
|
||||
windows-strings-0.1.0 \
|
||||
windows-sys-0.48.0 \
|
||||
windows-sys-0.52.0 \
|
||||
windows-sys-0.59.0 \
|
||||
windows-targets-0.48.5 \
|
||||
windows-targets-0.52.6 \
|
||||
windows_aarch64_gnullvm-0.41.0 \
|
||||
windows_aarch64_gnullvm-0.48.5 \
|
||||
windows_aarch64_gnullvm-0.52.6 \
|
||||
windows_aarch64_msvc-0.41.0 \
|
||||
windows_aarch64_msvc-0.48.5 \
|
||||
windows_aarch64_msvc-0.52.6 \
|
||||
windows_i686_gnu-0.41.0 \
|
||||
windows_i686_gnu-0.48.5 \
|
||||
windows_i686_gnu-0.52.6 \
|
||||
windows_i686_gnullvm-0.52.6 \
|
||||
windows_i686_msvc-0.41.0 \
|
||||
windows_i686_msvc-0.48.5 \
|
||||
windows_i686_msvc-0.52.6 \
|
||||
windows_x86_64_gnu-0.41.0 \
|
||||
windows_x86_64_gnu-0.48.5 \
|
||||
windows_x86_64_gnu-0.52.6 \
|
||||
windows_x86_64_gnullvm-0.41.0 \
|
||||
windows_x86_64_gnullvm-0.48.5 \
|
||||
windows_x86_64_gnullvm-0.52.6 \
|
||||
windows_x86_64_msvc-0.41.0 \
|
||||
windows_x86_64_msvc-0.48.5 \
|
||||
windows_x86_64_msvc-0.52.6 \
|
||||
winnow-0.5.40 \
|
||||
winnow-0.6.20 \
|
||||
winreg-0.50.0 \
|
||||
write16-1.0.0 \
|
||||
writeable-0.5.5 \
|
||||
x509-cert-0.2.5 \
|
||||
x509-parser-0.16.0 \
|
||||
yoke-0.7.5 \
|
||||
yoke-derive-0.7.5 \
|
||||
zerocopy-0.7.35 \
|
||||
zerocopy-derive-0.7.35 \
|
||||
zerofrom-0.1.5 \
|
||||
zerofrom-derive-0.1.5 \
|
||||
zeroize-1.8.1 \
|
||||
zeroize_derive-1.4.2 \
|
||||
zerovec-0.10.4 \
|
||||
zerovec-derive-0.10.3 \
|
||||
zip-0.6.6 \
|
||||
zxcvbn-2.2.2
|
File diff suppressed because it is too large
Load diff
|
@ -21,7 +21,7 @@ load_rc_config $name
|
|||
|
||||
pidfile="/var/run/kanidm-unixd-tasks.pid"
|
||||
command=/usr/sbin/daemon
|
||||
command_args="-u _kanidm_unixd -p /var/run/kanidm-unixd-tasks.pid -T kanidm_unixd_tasks /usr/local/libexec/${name}"
|
||||
command_args="-u root -p /var/run/kanidm-unixd-tasks.pid -T kanidm_unixd_tasks /usr/local/libexec/${name}"
|
||||
procname=/usr/local/libexec/${name}
|
||||
|
||||
run_rc_command "$1"
|
||||
|
|
|
@ -43,3 +43,6 @@ tracing = { workspace = true }
|
|||
[dev-dependencies]
|
||||
enum-iterator = { workspace = true }
|
||||
serde_urlencoded = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
kanidm_build_profiles = { workspace = true }
|
||||
|
|
3
proto/build.rs
Normal file
3
proto/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
profiles::apply_profile();
|
||||
}
|
|
@ -30,7 +30,7 @@ pub const VALID_IMAGE_UPLOAD_CONTENT_TYPES: [&str; 5] = [
|
|||
pub const APPLICATION_JSON: &str = "application/json";
|
||||
|
||||
/// The "system" path for Kanidm client config
|
||||
pub const DEFAULT_CLIENT_CONFIG_PATH: &str = "/etc/kanidm/config";
|
||||
pub const DEFAULT_CLIENT_CONFIG_PATH: &str = env!("KANIDM_CLIENT_CONFIG_PATH");
|
||||
/// The user-owned path for Kanidm client config
|
||||
pub const DEFAULT_CLIENT_CONFIG_PATH_HOME: &str = "~/.config/kanidm";
|
||||
|
||||
|
|
|
@ -443,6 +443,21 @@ fn require_request_uri_parameter_supported_default() -> bool {
|
|||
false
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct OidcWebfingerRel {
|
||||
pub rel: String,
|
||||
pub href: String,
|
||||
}
|
||||
|
||||
/// The response to an Webfinger request. Only a subset of the body is defined here.
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc7033#section-4.4>
|
||||
#[skip_serializing_none]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct OidcWebfingerResponse {
|
||||
pub subject: String,
|
||||
pub links: Vec<OidcWebfingerRel>,
|
||||
}
|
||||
|
||||
/// The response to an OpenID connect discovery request
|
||||
/// <https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata>
|
||||
#[skip_serializing_none]
|
||||
|
|
522
pykanidm/poetry.lock
generated
522
pykanidm/poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -29,7 +29,7 @@ Authlib = "^1.2.0"
|
|||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = ">=0.5.1,<0.9.5"
|
||||
ruff = ">=0.5.1,<0.9.6"
|
||||
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"
|
||||
mkdocstrings = ">=0.27,<0.29"
|
||||
mkdocstrings-python = "^1.13.0"
|
||||
pook = "^2.1.3"
|
||||
|
||||
|
|
|
@ -45,13 +45,14 @@ ldap3_proto = { workspace = true }
|
|||
libc = { workspace = true }
|
||||
openssl = { workspace = true }
|
||||
opentelemetry = { workspace = true, features = ["logs"] }
|
||||
# opentelemetry_api = { workspace = true, features = ["logs"] }
|
||||
qrcode = { workspace = true, features = ["svg"] }
|
||||
regex = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_with = { workspace = true }
|
||||
sketching = { workspace = true }
|
||||
sshkeys = { workspace = true }
|
||||
sshkey-attest = { workspace = true }
|
||||
time = { workspace = true, features = ["serde", "std", "local-offset"] }
|
||||
tokio = { workspace = true, features = ["net", "sync", "io-util", "macros"] }
|
||||
tokio-openssl = { workspace = true }
|
||||
|
@ -92,3 +93,10 @@ kanidmd_lib = { workspace = true, features = ["test"] }
|
|||
|
||||
[build-dependencies]
|
||||
kanidm_build_profiles = { workspace = true }
|
||||
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = [
|
||||
"opentelemetry", # feature gated
|
||||
"kanidm_build_profiles",
|
||||
]
|
||||
|
|
|
@ -9,6 +9,7 @@ use kanidm_proto::internal::{
|
|||
IdentifyUserRequest, IdentifyUserResponse, ImageValue, OperationError, RadiusAuthToken,
|
||||
SearchRequest, SearchResponse, UserAuthToken,
|
||||
};
|
||||
use kanidm_proto::oauth2::OidcWebfingerResponse;
|
||||
use kanidm_proto::v1::{
|
||||
AuthIssueSession, AuthRequest, Entry as ProtoEntry, UatStatus, UnixGroupToken, UnixUserToken,
|
||||
WhoamiResponse,
|
||||
|
@ -1509,6 +1510,21 @@ impl QueryServerReadV1 {
|
|||
idms_prox_read.oauth2_openid_discovery(&client_id)
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn handle_oauth2_webfinger_discovery(
|
||||
&self,
|
||||
client_id: &str,
|
||||
resource_id: &str,
|
||||
eventid: Uuid,
|
||||
) -> Result<OidcWebfingerResponse, OperationError> {
|
||||
let mut idms_prox_read = self.idms.proxy_read().await?;
|
||||
idms_prox_read.oauth2_openid_webfinger(client_id, resource_id)
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
|
|
|
@ -513,7 +513,7 @@ pub async fn oauth2_token_post(
|
|||
}
|
||||
}
|
||||
|
||||
// // For future openid integration
|
||||
// For future openid integration
|
||||
pub async fn oauth2_openid_discovery_get(
|
||||
State(state): State<ServerState>,
|
||||
Path(client_id): Path<String>,
|
||||
|
@ -538,6 +538,46 @@ pub async fn oauth2_openid_discovery_get(
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Oauth2OpenIdWebfingerQuery {
|
||||
resource: String,
|
||||
}
|
||||
|
||||
pub async fn oauth2_openid_webfinger_get(
|
||||
State(state): State<ServerState>,
|
||||
Path(client_id): Path<String>,
|
||||
Query(query): Query<Oauth2OpenIdWebfingerQuery>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
) -> impl IntoResponse {
|
||||
let Oauth2OpenIdWebfingerQuery { resource } = query;
|
||||
|
||||
let cleaned_resource = resource.strip_prefix("acct:").unwrap_or(&resource);
|
||||
|
||||
let res = state
|
||||
.qe_r_ref
|
||||
.handle_oauth2_webfinger_discovery(&client_id, cleaned_resource, kopid.eventid)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(mut dsc) => (
|
||||
StatusCode::OK,
|
||||
[
|
||||
(ACCESS_CONTROL_ALLOW_ORIGIN, "*"),
|
||||
(CONTENT_TYPE, "application/jrd+json"),
|
||||
],
|
||||
Json({
|
||||
dsc.subject = resource;
|
||||
dsc
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => {
|
||||
error!(err = ?e, "Unable to access discovery info");
|
||||
WebError::from(e).response_with_access_control_origin_header()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn oauth2_rfc8414_metadata_get(
|
||||
State(state): State<ServerState>,
|
||||
Path(client_id): Path<String>,
|
||||
|
@ -770,6 +810,10 @@ pub fn route_setup(state: ServerState) -> Router<ServerState> {
|
|||
"/oauth2/openid/:client_id/.well-known/openid-configuration",
|
||||
get(oauth2_openid_discovery_get).options(oauth2_preflight_options),
|
||||
)
|
||||
.route(
|
||||
"/oauth2/openid/:client_id/.well-known/webfinger",
|
||||
get(oauth2_openid_webfinger_get).options(oauth2_preflight_options),
|
||||
)
|
||||
// // ⚠️ ⚠️ WARNING ⚠️ ⚠️
|
||||
// // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
|
||||
.route(
|
||||
|
|
|
@ -200,6 +200,7 @@ pub(crate) async fn oauth2_id_scopemap_post(
|
|||
Json(scopes): Json<Vec<String>>,
|
||||
) -> Result<Json<()>, WebError> {
|
||||
let filter = oauth2_id(&rs_name);
|
||||
|
||||
state
|
||||
.qe_w_ref
|
||||
.handle_oauth2_scopemap_update(client_auth_info, group, scopes, filter, kopid.eventid)
|
||||
|
|
|
@ -112,6 +112,10 @@ pub fn view_router() -> Router<ServerState> {
|
|||
.route("/reset/change_password", post(reset::view_new_pwd))
|
||||
.route("/reset/add_passkey", post(reset::view_new_passkey))
|
||||
.route("/reset/set_unixcred", post(reset::view_set_unixcred))
|
||||
.route(
|
||||
"/reset/add_ssh_publickey",
|
||||
post(reset::view_add_ssh_publickey),
|
||||
)
|
||||
.route("/api/delete_alt_creds", post(reset::remove_alt_creds))
|
||||
.route("/api/delete_unixcred", post(reset::remove_unixcred))
|
||||
.route("/api/add_totp", post(reset::add_totp))
|
||||
|
@ -119,6 +123,10 @@ pub fn view_router() -> Router<ServerState> {
|
|||
.route("/api/remove_passkey", post(reset::remove_passkey))
|
||||
.route("/api/finish_passkey", post(reset::finish_passkey))
|
||||
.route("/api/cancel_mfareg", post(reset::cancel_mfareg))
|
||||
.route(
|
||||
"/api/remove_ssh_publickey",
|
||||
post(reset::remove_ssh_publickey),
|
||||
)
|
||||
.route("/api/cu_cancel", post(reset::cancel_cred_update))
|
||||
.route("/api/cu_commit", post(reset::commit))
|
||||
.layer(HxRequestGuardLayer::new("/ui"));
|
||||
|
|
|
@ -14,11 +14,15 @@ use qrcode::render::svg;
|
|||
use qrcode::QrCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::skip_serializing_none;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub use sshkey_attest::proto::PublicKey as SshPublicKey;
|
||||
pub use sshkeys::KeyType;
|
||||
|
||||
use kanidm_proto::internal::{
|
||||
CUCredState, CUExtPortal, CURegState, CURegWarning, CURequest, CUSessionToken, CUStatus,
|
||||
CredentialDetail, OperationError, PasskeyDetail, PasswordFeedback, TotpAlgo, UserAuthToken,
|
||||
|
@ -69,6 +73,12 @@ struct CredStatusView {
|
|||
credentials_update_partial: CredResetPartialView,
|
||||
}
|
||||
|
||||
struct SshKey {
|
||||
key_type: KeyType,
|
||||
key: String,
|
||||
comment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "credentials_update_partial.html")]
|
||||
struct CredResetPartialView {
|
||||
|
@ -83,6 +93,8 @@ struct CredResetPartialView {
|
|||
primary: Option<CredentialDetail>,
|
||||
unixcred_state: CUCredState,
|
||||
unixcred: Option<CredentialDetail>,
|
||||
sshkeys_state: CUCredState,
|
||||
sshkeys: BTreeMap<String, SshKey>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
|
@ -104,6 +116,13 @@ struct SetUnixCredPartial {
|
|||
check_res: PwdCheckResult,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "credential_update_add_ssh_publickey_partial.html")]
|
||||
struct AddSshPublicKeyPartial {
|
||||
title_error: Option<String>,
|
||||
key_error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
enum PwdCheckResult {
|
||||
Success,
|
||||
|
@ -120,6 +139,17 @@ pub(crate) struct NewPassword {
|
|||
new_password_check: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(crate) struct NewPublicKey {
|
||||
title: String,
|
||||
key: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(crate) struct PublicKeyRemoveData {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(crate) struct NewTotp {
|
||||
name: String,
|
||||
|
@ -341,6 +371,30 @@ pub(crate) async fn remove_unixcred(
|
|||
Ok(get_cu_partial_response(cu_status))
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_ssh_publickey(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
HxRequest(_hx_request): HxRequest,
|
||||
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
|
||||
DomainInfo(domain_info): DomainInfo,
|
||||
jar: CookieJar,
|
||||
Form(publickey): Form<PublicKeyRemoveData>,
|
||||
) -> axum::response::Result<Response> {
|
||||
let cu_session_token: CUSessionToken = get_cu_session(&jar).await?;
|
||||
|
||||
let cu_status = state
|
||||
.qe_r_ref
|
||||
.handle_idmcredentialupdate(
|
||||
cu_session_token,
|
||||
CURequest::SshPublicKeyRemove(publickey.name),
|
||||
kopid.eventid,
|
||||
)
|
||||
.map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info))
|
||||
.await?;
|
||||
|
||||
Ok(get_cu_partial_response(cu_status))
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_totp(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
|
@ -805,6 +859,95 @@ pub(crate) async fn view_set_unixcred(
|
|||
.into_response())
|
||||
}
|
||||
|
||||
struct AddSshPublicKeyError {
|
||||
key: Option<String>,
|
||||
title: Option<String>,
|
||||
}
|
||||
|
||||
pub(crate) async fn view_add_ssh_publickey(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
HxRequest(_hx_request): HxRequest,
|
||||
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
|
||||
DomainInfo(domain_info): DomainInfo,
|
||||
jar: CookieJar,
|
||||
opt_form: Option<Form<NewPublicKey>>,
|
||||
) -> axum::response::Result<Response> {
|
||||
let cu_session_token: CUSessionToken = get_cu_session(&jar).await?;
|
||||
|
||||
let new_key = match opt_form {
|
||||
None => {
|
||||
return Ok((AddSshPublicKeyPartial {
|
||||
title_error: None,
|
||||
key_error: None,
|
||||
},)
|
||||
.into_response());
|
||||
}
|
||||
Some(Form(new_key)) => new_key,
|
||||
};
|
||||
|
||||
let (
|
||||
AddSshPublicKeyError {
|
||||
key: key_error,
|
||||
title: title_error,
|
||||
},
|
||||
status,
|
||||
) = {
|
||||
let publickey = match SshPublicKey::from_string(&new_key.key) {
|
||||
Err(_) => {
|
||||
return Ok((AddSshPublicKeyPartial {
|
||||
title_error: None,
|
||||
key_error: Some("Key cannot be parsed".to_string()),
|
||||
},)
|
||||
.into_response());
|
||||
}
|
||||
Ok(publickey) => publickey,
|
||||
};
|
||||
let res = state
|
||||
.qe_r_ref
|
||||
.handle_idmcredentialupdate(
|
||||
cu_session_token,
|
||||
CURequest::SshPublicKey(new_key.title, publickey),
|
||||
kopid.eventid,
|
||||
)
|
||||
.await;
|
||||
match res {
|
||||
Ok(cu_status) => return Ok(get_cu_partial_response(cu_status)),
|
||||
Err(e @ (OperationError::InvalidLabel | OperationError::DuplicateLabel)) => (
|
||||
AddSshPublicKeyError {
|
||||
title: Some(e.to_string()),
|
||||
key: None,
|
||||
},
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
),
|
||||
Err(e @ OperationError::DuplicateKey) => (
|
||||
AddSshPublicKeyError {
|
||||
key: Some(e.to_string()),
|
||||
title: None,
|
||||
},
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
),
|
||||
Err(operr) => {
|
||||
return Err(ErrorResponse::from(HtmxError::new(
|
||||
&kopid,
|
||||
operr,
|
||||
domain_info,
|
||||
)))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok((
|
||||
status,
|
||||
HxPushUrl(Uri::from_static("/ui/reset/add_ssh_publickey")),
|
||||
AddSshPublicKeyPartial {
|
||||
title_error,
|
||||
key_error,
|
||||
},
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(crate) async fn view_reset_get(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
|
@ -910,9 +1053,25 @@ fn get_cu_partial(cu_status: CUStatus) -> CredResetPartialView {
|
|||
primary,
|
||||
unixcred_state,
|
||||
unixcred,
|
||||
sshkeys_state,
|
||||
sshkeys,
|
||||
..
|
||||
} = cu_status;
|
||||
|
||||
let sshkeyss: BTreeMap<String, SshKey> = sshkeys
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k.clone(),
|
||||
SshKey {
|
||||
key_type: v.clone().key_type,
|
||||
key: v.fingerprint().hash,
|
||||
comment: v.comment.clone(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
CredResetPartialView {
|
||||
ext_cred_portal,
|
||||
can_commit,
|
||||
|
@ -925,6 +1084,8 @@ fn get_cu_partial(cu_status: CUStatus) -> CredResetPartialView {
|
|||
primary,
|
||||
unixcred_state,
|
||||
unixcred,
|
||||
sshkeys_state,
|
||||
sshkeys: sshkeyss,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -88,15 +88,6 @@
|
|||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<rect
|
||||
style="fill:#ffffff;stroke-width:0.243721"
|
||||
id="rect443"
|
||||
width="135.46666"
|
||||
height="135.46666"
|
||||
x="0"
|
||||
y="0"
|
||||
inkscape:label="background"
|
||||
sodipodi:insensitive="true" />
|
||||
<g
|
||||
id="layer1"
|
||||
transform="matrix(0.91407203,0,0,0.91407203,-34.121105,-24.362694)"
|
||||
|
|
Before (image error) Size: 16 KiB After (image error) Size: 16 KiB |
|
@ -199,3 +199,10 @@ footer {
|
|||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
}
|
||||
|
||||
.ssh-list-icon {
|
||||
--icon-size: 32px;
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
transform: rotate(35deg);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<hr>
|
||||
<div class="d-flex flex-column row-gap-4">
|
||||
<h4>Add new SSH Key</h4>
|
||||
<form class="row-gap-3 d-flex flex-column needs-validation"
|
||||
hx-target="#credentialUpdateDynamicSection"
|
||||
hx-post="/ui/reset/add_ssh_publickey">
|
||||
<div>
|
||||
<label for="key-title" class="form-label">Title</label>
|
||||
<input type="text" class="form-control(% if let Some(_) = title_error %) is-invalid(% endif %)" id="key-title" name="title" aria-describedby="title-validation-feedback">
|
||||
<div id="title-validation-feedback" class="invalid-feedback">
|
||||
(% if let Some(title_error) = title_error %)(( title_error ))(% endif %)
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="key-content" class="form-label">Key</label>
|
||||
<textarea class="form-control(% if let Some(_) = key_error %) is-invalid(% endif %)" id="key-content" rows="5" name="key"
|
||||
aria-describedby="key-validation-feedback"
|
||||
placeholder="Begins with 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519', 'sk-ecdsa-sha2-nistp256@openssh.com', or 'sk-ssh-ed25519@openssh.com'"
|
||||
></textarea>
|
||||
<div id="key-validation-feedback" class="invalid-feedback">
|
||||
(% if let Some(key_error) = key_error %)(( key_error ))(% endif %)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column-gap-2 d-flex justify-content-end mt-2" hx-target="#credentialUpdateDynamicSection">
|
||||
<button type="button" class="btn btn-danger" hx-get=((Urls::CredReset)) hx-target="body">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -85,12 +85,14 @@
|
|||
(% when CUCredState::Modifiable %)
|
||||
(% include "credentials_update_passkeys.html" %)
|
||||
<!-- Here we are modifiable so we can render the button to add passkeys -->
|
||||
<button type="button" class="btn btn-primary"
|
||||
hx-post="/ui/reset/add_passkey"
|
||||
hx-vals='{"class": "Any"}'
|
||||
hx-target="#credentialUpdateDynamicSection">
|
||||
Add Passkey
|
||||
</button>
|
||||
<div class="mt-3">
|
||||
<button type="button" class="btn btn-primary"
|
||||
hx-post="/ui/reset/add_passkey"
|
||||
hx-vals='{"class": "Any"}'
|
||||
hx-target="#credentialUpdateDynamicSection">
|
||||
Add Passkey
|
||||
</button>
|
||||
</div>
|
||||
|
||||
(% when CUCredState::DeleteOnly %)
|
||||
(% if passkeys.len() > 0 %)
|
||||
|
@ -134,6 +136,55 @@
|
|||
(% when CUCredState::PolicyDeny %)
|
||||
(% endmatch %)
|
||||
|
||||
(% match sshkeys_state %)
|
||||
(% when CUCredState::Modifiable %)
|
||||
<hr class="my-4" />
|
||||
<h4>SSH Keys</h4>
|
||||
(% if sshkeys.len() > 0 %)
|
||||
<p>This is a list of SSH keys associated with your account.</p>
|
||||
<ul class="list-group">
|
||||
(% for (keyname, sshkey) in sshkeys %)
|
||||
<li class="list-group-item d-flex column-gap-3 py-3">
|
||||
<div>
|
||||
<img class="ssh-list-icon" src="/pkg/img/icons/key.svg" alt="" />
|
||||
</div>
|
||||
<div class="d-flex flex-column row-gap-2 flex-grow-1">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="fw-bold column-gap-2">
|
||||
(( keyname ))<span class="badge rounded-pill text-bg-dark ms-2">(( sshkey.key_type.short_name ))</span>
|
||||
</div>
|
||||
<button class="btn btn-tiny btn-danger"
|
||||
hx-post="/ui/api/remove_ssh_publickey"
|
||||
hx-vals='{"name": "(( keyname ))"}'
|
||||
hx-target="#credentialUpdateDynamicSection">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
<div><span class="font-monospace text-break">SHA256:(( sshkey.key ))</span></div>
|
||||
(% if let Some(comment) = sshkey.comment %)
|
||||
<div class="rounded bg-body-tertiary border border-light-subtle text-body-secondary px-2 py-1 align-self-stretch">Comment: (( comment ))</div>
|
||||
(% endif %)
|
||||
</div>
|
||||
</li>
|
||||
(% endfor %)
|
||||
</ul>
|
||||
(% else %)
|
||||
<p>There are no SSH keys associated with your account.</p>
|
||||
(% endif %)
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-primary" type="button"
|
||||
hx-post="/ui/reset/add_ssh_publickey"
|
||||
hx-target="#credentialUpdateDynamicSection">
|
||||
Add SSH Key
|
||||
</button>
|
||||
</div>
|
||||
|
||||
(% when CUCredState::DeleteOnly %)
|
||||
(% when CUCredState::AccessDeny %)
|
||||
(% when CUCredState::PolicyDeny %)
|
||||
(% endmatch %)
|
||||
|
||||
|
||||
<hr class="my-4" />
|
||||
<div id="cred-update-commit-bar" class="toast bs-emphasis-color bs-secondary-bg">
|
||||
<div class="toast-body">
|
||||
|
@ -148,7 +199,7 @@
|
|||
</svg>
|
||||
<b>Careful</b> - Unsaved changes will be lost</div>
|
||||
</span>
|
||||
<div class="mt-2 pt-2 border-top">
|
||||
<div class="mt-3 d-flex column-gap-2">
|
||||
<button class="btn btn-danger"
|
||||
hx-post="/ui/api/cu_cancel"
|
||||
hx-boost="false"
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-md-0">
|
||||
<ul class="navbar-nav">
|
||||
<li>
|
||||
<a class="nav-link" href=((Urls::Apps))>
|
||||
<span data-feather="file"></span>Applications</a>
|
||||
|
@ -31,7 +31,7 @@
|
|||
<span data-feather="file"></span>Profile</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav me-auto mb-2 mb-md-0 ms-md-auto">
|
||||
<ul class="navbar-nav ms-md-auto">
|
||||
<li>
|
||||
<a class="nav-link" href="#" data-bs-toggle="modal"
|
||||
data-bs-target="#signoutModal">Sign out</a>
|
||||
|
|
|
@ -37,11 +37,11 @@ reqwest = { workspace = true }
|
|||
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
tracing = { workspace = true }
|
||||
serde_json.workspace = true
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
sd-notify.workspace = true
|
||||
prctl.workspace = true
|
||||
sd-notify = { workspace = true }
|
||||
prctl = { workspace = true }
|
||||
|
||||
[target.'cfg(target_family = "windows")'.dependencies]
|
||||
whoami = { workspace = true }
|
||||
|
@ -53,7 +53,10 @@ kanidm_utils_users = { workspace = true }
|
|||
mimalloc = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
clap_complete = { workspace = true }
|
||||
kanidm_build_profiles = { workspace = true }
|
||||
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["clap_complete", "kanidm_build_profiles"]
|
||||
|
|
|
@ -20,7 +20,7 @@ static ALLOC: dhat::Alloc = dhat::Alloc;
|
|||
|
||||
use std::fs::{metadata, File};
|
||||
// This works on both unix and windows.
|
||||
use fs4::FileExt;
|
||||
use fs4::fs_std::FileExt;
|
||||
use kanidm_proto::messages::ConsoleOutputMode;
|
||||
use sketching::otel::TracingPipelineGuard;
|
||||
use std::io::Read;
|
||||
|
|
|
@ -79,7 +79,7 @@ webauthn-rs = { workspace = true, features = [
|
|||
webauthn-rs-core = { workspace = true }
|
||||
zxcvbn = { workspace = true }
|
||||
serde_with = { workspace = true, features = ["time_0_3", "base64"] }
|
||||
hex.workspace = true
|
||||
hex = { workspace = true }
|
||||
lodepng = { workspace = true }
|
||||
image = { workspace = true, default-features = false, features = [
|
||||
"gif",
|
||||
|
@ -113,3 +113,9 @@ mimalloc = { workspace = true }
|
|||
hashbrown = { workspace = true }
|
||||
kanidm_build_profiles = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = [
|
||||
"openssl-sys", # see note above
|
||||
"whoami", # used in windows
|
||||
]
|
||||
|
|
|
@ -571,6 +571,10 @@ pub trait BackendTransaction {
|
|||
filter_error!("Requested a top level or isolated AndNot, returning empty");
|
||||
(IdList::Indexed(IDLBitRange::new()), FilterPlan::Invalid)
|
||||
}
|
||||
FilterResolved::Invalid(_) => {
|
||||
// Indexed since it is always false and we don't want to influence filter testing
|
||||
(IdList::Indexed(IDLBitRange::new()), FilterPlan::Invalid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -2376,6 +2380,46 @@ mod tests {
|
|||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_be_search_with_invalid() {
|
||||
run_test!(|be: &mut BackendWriteTransaction| {
|
||||
trace!("Simple Search");
|
||||
|
||||
let mut e: Entry<EntryInit, EntryNew> = Entry::new();
|
||||
e.add_ava(Attribute::UserId, Value::from("bagel"));
|
||||
e.add_ava(
|
||||
Attribute::Uuid,
|
||||
Value::from("db237e8a-0079-4b8c-8a56-593b22aa44d1"),
|
||||
);
|
||||
let e = e.into_sealed_new();
|
||||
|
||||
let single_result = be.create(&CID_ZERO, vec![e]);
|
||||
assert!(single_result.is_ok());
|
||||
|
||||
// Test Search with or condition including invalid attribute
|
||||
let filt = filter_resolved!(f_or(vec![
|
||||
f_eq(Attribute::UserId, PartialValue::new_utf8s("bagel")),
|
||||
f_invalid(Attribute::UserId)
|
||||
]));
|
||||
|
||||
let lims = Limits::unlimited();
|
||||
|
||||
let r = be.search(&lims, &filt);
|
||||
assert!(r.expect("Search failed!").len() == 1);
|
||||
|
||||
// Test Search with or condition including invalid attribute
|
||||
let filt = filter_resolved!(f_and(vec![
|
||||
f_eq(Attribute::UserId, PartialValue::new_utf8s("bagel")),
|
||||
f_invalid(Attribute::UserId)
|
||||
]));
|
||||
|
||||
let lims = Limits::unlimited();
|
||||
|
||||
let r = be.search(&lims, &filt);
|
||||
assert!(r.expect("Search failed!").len() == 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_be_simple_modify() {
|
||||
run_test!(|be: &mut BackendWriteTransaction| {
|
||||
|
|
|
@ -289,15 +289,6 @@ lazy_static! {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
/// Self-write of mail
|
||||
pub static ref IDM_PEOPLE_SELF_WRITE_MAIL_V1: BuiltinGroup = BuiltinGroup {
|
||||
name: "idm_people_self_write_mail",
|
||||
description: "Builtin IDM Group for people accounts to update their own mail.",
|
||||
uuid: UUID_IDM_PEOPLE_SELF_MAIL_WRITE,
|
||||
members: Vec::with_capacity(0),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
/// Self-write of mail
|
||||
pub static ref IDM_PEOPLE_SELF_MAIL_WRITE_DL7: BuiltinGroup = BuiltinGroup {
|
||||
name: "idm_people_self_mail_write",
|
||||
|
@ -373,36 +364,7 @@ lazy_static! {
|
|||
};
|
||||
|
||||
/// This must be the last group to init to include the UUID of the other high priv groups.
|
||||
pub static ref IDM_HIGH_PRIVILEGE_V1: BuiltinGroup = BuiltinGroup {
|
||||
name: "idm_high_privilege",
|
||||
uuid: UUID_IDM_HIGH_PRIVILEGE,
|
||||
entry_managed_by: Some(UUID_IDM_ACCESS_CONTROL_ADMINS),
|
||||
description: "Builtin IDM provided groups with high levels of access that should be audited and limited in modification.",
|
||||
members: vec![
|
||||
UUID_SYSTEM_ADMINS,
|
||||
UUID_IDM_ADMINS,
|
||||
UUID_DOMAIN_ADMINS,
|
||||
UUID_IDM_SERVICE_DESK,
|
||||
UUID_IDM_RECYCLE_BIN_ADMINS,
|
||||
UUID_IDM_SCHEMA_ADMINS,
|
||||
UUID_IDM_ACCESS_CONTROL_ADMINS,
|
||||
UUID_IDM_OAUTH2_ADMINS,
|
||||
UUID_IDM_RADIUS_ADMINS,
|
||||
UUID_IDM_ACCOUNT_POLICY_ADMINS,
|
||||
UUID_IDM_RADIUS_SERVERS,
|
||||
UUID_IDM_GROUP_ADMINS,
|
||||
UUID_IDM_UNIX_ADMINS,
|
||||
UUID_IDM_PEOPLE_PII_READ,
|
||||
UUID_IDM_PEOPLE_ADMINS,
|
||||
UUID_IDM_PEOPLE_ON_BOARDING,
|
||||
UUID_IDM_SERVICE_ACCOUNT_ADMINS,
|
||||
UUID_IDM_HIGH_PRIVILEGE,
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
/// This must be the last group to init to include the UUID of the other high priv groups.
|
||||
pub static ref IDM_HIGH_PRIVILEGE_DL7: BuiltinGroup = BuiltinGroup {
|
||||
pub static ref IDM_HIGH_PRIVILEGE_DL8: BuiltinGroup = BuiltinGroup {
|
||||
name: "idm_high_privilege",
|
||||
uuid: UUID_IDM_HIGH_PRIVILEGE,
|
||||
entry_managed_by: Some(UUID_IDM_ACCESS_CONTROL_ADMINS),
|
||||
|
@ -426,12 +388,14 @@ lazy_static! {
|
|||
UUID_IDM_PEOPLE_ON_BOARDING,
|
||||
UUID_IDM_SERVICE_ACCOUNT_ADMINS,
|
||||
UUID_IDM_CLIENT_CERTIFICATE_ADMINS,
|
||||
UUID_IDM_APPLICATION_ADMINS,
|
||||
UUID_IDM_MAIL_ADMINS,
|
||||
UUID_IDM_HIGH_PRIVILEGE,
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
pub static ref BUILTIN_GROUP_APPLICATION_ADMINS: BuiltinGroup = BuiltinGroup {
|
||||
pub static ref BUILTIN_GROUP_APPLICATION_ADMINS_DL8: BuiltinGroup = BuiltinGroup {
|
||||
name: "idm_application_admins",
|
||||
uuid: UUID_IDM_APPLICATION_ADMINS,
|
||||
description: "Builtin Application Administration Group.",
|
||||
|
@ -458,17 +422,19 @@ pub fn idm_builtin_non_admin_groups() -> Vec<&'static BuiltinGroup> {
|
|||
&BUILTIN_GROUP_PEOPLE_PII_READ,
|
||||
&BUILTIN_GROUP_PEOPLE_ON_BOARDING,
|
||||
&BUILTIN_GROUP_SERVICE_ACCOUNT_ADMINS,
|
||||
&BUILTIN_GROUP_APPLICATION_ADMINS,
|
||||
&BUILTIN_GROUP_MAIL_SERVICE_ADMINS_DL8,
|
||||
&IDM_GROUP_ADMINS_V1,
|
||||
&IDM_ALL_PERSONS,
|
||||
&IDM_ALL_ACCOUNTS,
|
||||
&BUILTIN_IDM_RADIUS_SERVERS_V1,
|
||||
&BUILTIN_IDM_MAIL_SERVERS_DL8,
|
||||
&IDM_PEOPLE_SELF_WRITE_MAIL_V1,
|
||||
&BUILTIN_GROUP_PEOPLE_SELF_NAME_WRITE_DL7,
|
||||
&IDM_PEOPLE_SELF_MAIL_WRITE_DL7,
|
||||
&BUILTIN_GROUP_CLIENT_CERTIFICATE_ADMINS_DL7,
|
||||
&BUILTIN_GROUP_APPLICATION_ADMINS_DL8,
|
||||
// Write deps on read, so write must be added first.
|
||||
// All members must exist before we write HP
|
||||
&IDM_HIGH_PRIVILEGE_V1,
|
||||
&IDM_HIGH_PRIVILEGE_DL8,
|
||||
// other things
|
||||
&IDM_UI_ENABLE_EXPERIMENTAL_FEATURES,
|
||||
&IDM_ACCOUNT_MAIL_READ,
|
||||
|
|
|
@ -54,14 +54,6 @@ pub type DomainVersion = u32;
|
|||
/// previously.
|
||||
pub const DOMAIN_LEVEL_0: DomainVersion = 0;
|
||||
|
||||
/// Deprecated as of 1.3.0
|
||||
pub const DOMAIN_LEVEL_5: DomainVersion = 5;
|
||||
|
||||
/// Domain Level introduced with 1.2.0.
|
||||
/// Deprecated as of 1.4.0
|
||||
pub const DOMAIN_LEVEL_6: DomainVersion = 6;
|
||||
pub const PATCH_LEVEL_1: u32 = 1;
|
||||
|
||||
/// Domain Level introduced with 1.3.0.
|
||||
/// Deprecated as of 1.5.0
|
||||
pub const DOMAIN_LEVEL_7: DomainVersion = 7;
|
||||
|
@ -79,22 +71,28 @@ pub const PATCH_LEVEL_2: u32 = 2;
|
|||
/// Deprecated as of 1.8.0
|
||||
pub const DOMAIN_LEVEL_10: DomainVersion = 10;
|
||||
|
||||
/// Domain Level introduced with 1.7.0.
|
||||
/// Deprecated as of 1.9.0
|
||||
pub const DOMAIN_LEVEL_11: DomainVersion = 11;
|
||||
|
||||
// The minimum level that we can re-migrate from.
|
||||
// This should be DOMAIN_TGT_LEVEL minus 2
|
||||
pub const DOMAIN_MIN_REMIGRATION_LEVEL: DomainVersion = DOMAIN_LEVEL_7;
|
||||
pub const DOMAIN_MIN_REMIGRATION_LEVEL: DomainVersion = DOMAIN_LEVEL_8;
|
||||
// The minimum supported domain functional level (for replication)
|
||||
pub const DOMAIN_MIN_LEVEL: DomainVersion = DOMAIN_TGT_LEVEL;
|
||||
// The previous releases domain functional level
|
||||
pub const DOMAIN_PREVIOUS_TGT_LEVEL: DomainVersion = DOMAIN_LEVEL_8;
|
||||
pub const DOMAIN_PREVIOUS_TGT_LEVEL: DomainVersion = DOMAIN_TGT_LEVEL - 1;
|
||||
// The target supported domain functional level. During development this is
|
||||
// the NEXT level that users will upgrade too.
|
||||
pub const DOMAIN_TGT_LEVEL: DomainVersion = DOMAIN_LEVEL_9;
|
||||
// the NEXT level that users will upgrade too. In other words if we are
|
||||
// developing 1.6.0-dev, then we need to set TGT_LEVEL to 10 which is
|
||||
// the corresponding level.
|
||||
pub const DOMAIN_TGT_LEVEL: DomainVersion = DOMAIN_LEVEL_10;
|
||||
// The current patch level if any out of band fixes are required.
|
||||
pub const DOMAIN_TGT_PATCH_LEVEL: u32 = PATCH_LEVEL_2;
|
||||
// The target domain functional level for the SUBSEQUENT release/dev cycle.
|
||||
pub const DOMAIN_TGT_NEXT_LEVEL: DomainVersion = DOMAIN_LEVEL_10;
|
||||
pub const DOMAIN_TGT_NEXT_LEVEL: DomainVersion = DOMAIN_TGT_LEVEL + 1;
|
||||
// The maximum supported domain functional level
|
||||
pub const DOMAIN_MAX_LEVEL: DomainVersion = DOMAIN_LEVEL_10;
|
||||
pub const DOMAIN_MAX_LEVEL: DomainVersion = DOMAIN_LEVEL_11;
|
||||
|
||||
// On test builds define to 60 seconds
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -1209,6 +1209,30 @@ pub static ref SCHEMA_CLASS_DOMAIN_INFO_DL9: SchemaClass = SchemaClass {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
pub static ref SCHEMA_CLASS_DOMAIN_INFO_DL10: 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,
|
||||
Attribute::DomainDisplayName,
|
||||
],
|
||||
systemmust: vec![
|
||||
Attribute::Name,
|
||||
Attribute::DomainUuid,
|
||||
Attribute::DomainName,
|
||||
Attribute::Version,
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
pub static ref SCHEMA_CLASS_POSIXGROUP: SchemaClass = SchemaClass {
|
||||
uuid: UUID_SCHEMA_CLASS_POSIXGROUP,
|
||||
name: EntryClass::PosixGroup.into(),
|
||||
|
|
|
@ -2912,6 +2912,7 @@ impl<VALID, STATE> Entry<VALID, STATE> {
|
|||
false
|
||||
}
|
||||
FilterResolved::AndNot(f, _) => !self.entry_match_no_index_inner(f),
|
||||
FilterResolved::Invalid(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -83,6 +83,10 @@ pub fn f_self() -> FC {
|
|||
FC::SelfUuid
|
||||
}
|
||||
|
||||
pub fn f_invalid(a: Attribute) -> FC {
|
||||
FC::Invalid(a)
|
||||
}
|
||||
|
||||
pub fn f_id(uuid: &str) -> FC {
|
||||
let uf = Uuid::parse_str(uuid)
|
||||
.ok()
|
||||
|
@ -117,6 +121,7 @@ pub enum FC {
|
|||
Inclusion(Vec<FC>),
|
||||
AndNot(Box<FC>),
|
||||
SelfUuid,
|
||||
Invalid(Attribute),
|
||||
// Not(Box<FC>),
|
||||
}
|
||||
|
||||
|
@ -135,6 +140,7 @@ enum FilterComp {
|
|||
Inclusion(Vec<FilterComp>),
|
||||
AndNot(Box<FilterComp>),
|
||||
SelfUuid,
|
||||
Invalid(Attribute),
|
||||
// Does this mean we can add a true not to the type now?
|
||||
// Not(Box<FilterComp>),
|
||||
}
|
||||
|
@ -196,12 +202,15 @@ impl fmt::Debug for FilterComp {
|
|||
FilterComp::SelfUuid => {
|
||||
write!(f, "uuid eq self")
|
||||
}
|
||||
FilterComp::Invalid(attr) => {
|
||||
write!(f, "invalid ( {:?} )", attr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This is the fully resolved internal representation. Note the lack of Not and selfUUID
|
||||
/// because these are resolved into And(Pres(class), AndNot(term)) and Eq(uuid, ...).
|
||||
/// because these are resolved into And(Pres(class), AndNot(term)) and Eq(uuid, ...) respectively.
|
||||
/// Importantly, we make this accessible to Entry so that it can then match on filters
|
||||
/// internally.
|
||||
///
|
||||
|
@ -221,6 +230,7 @@ pub enum FilterResolved {
|
|||
LessThan(Attribute, PartialValue, Option<NonZeroU8>),
|
||||
Or(Vec<FilterResolved>, Option<NonZeroU8>),
|
||||
And(Vec<FilterResolved>, Option<NonZeroU8>),
|
||||
Invalid(Attribute),
|
||||
// All terms must have 1 or more items, or the inclusion is false!
|
||||
Inclusion(Vec<FilterResolved>, Option<NonZeroU8>),
|
||||
AndNot(Box<FilterResolved>, Option<NonZeroU8>),
|
||||
|
@ -310,6 +320,9 @@ impl fmt::Debug for FilterResolved {
|
|||
FilterResolved::AndNot(inner, idx) => {
|
||||
write!(f, "not (s{} {:?})", idx.unwrap_or(NonZeroU8::MAX), inner)
|
||||
}
|
||||
FilterResolved::Invalid(attr) => {
|
||||
write!(f, "{} inv", attr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -777,6 +790,7 @@ impl FilterComp {
|
|||
FC::Inclusion(v) => FilterComp::Inclusion(v.into_iter().map(FilterComp::new).collect()),
|
||||
FC::AndNot(b) => FilterComp::AndNot(Box::new(FilterComp::new(*b))),
|
||||
FC::SelfUuid => FilterComp::SelfUuid,
|
||||
FC::Invalid(a) => FilterComp::Invalid(a),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -804,7 +818,8 @@ impl FilterComp {
|
|||
| FilterComp::Stw(attr, _)
|
||||
| FilterComp::Enw(attr, _)
|
||||
| FilterComp::Pres(attr)
|
||||
| FilterComp::LessThan(attr, _) => {
|
||||
| FilterComp::LessThan(attr, _)
|
||||
| FilterComp::Invalid(attr) => {
|
||||
r_set.insert(attr.clone());
|
||||
}
|
||||
FilterComp::Or(vs) => vs.iter().for_each(|f| f.get_attr_set(r_set)),
|
||||
|
@ -952,6 +967,11 @@ impl FilterComp {
|
|||
// Pretty hard to mess this one up ;)
|
||||
Ok(FilterComp::SelfUuid)
|
||||
}
|
||||
FilterComp::Invalid(attr) => {
|
||||
// FilterComp may be invalid but Invalid is still a valid value.
|
||||
// we continue the evaluation so OR queries can still succeed
|
||||
Ok(FilterComp::Invalid(attr.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1097,8 +1117,13 @@ impl FilterComp {
|
|||
}
|
||||
LdapFilter::Equality(a, v) => {
|
||||
let a = ldap_attr_filter_map(a);
|
||||
let v = qs.clone_partialvalue(&a, v)?;
|
||||
FilterComp::Eq(a, v)
|
||||
let pv = qs.clone_partialvalue(&a, v);
|
||||
|
||||
match pv {
|
||||
Ok(pv) => FilterComp::Eq(a, pv),
|
||||
Err(_) if a == Attribute::Spn => FilterComp::Invalid(a),
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
LdapFilter::Present(a) => FilterComp::Pres(ldap_attr_filter_map(a)),
|
||||
LdapFilter::Substring(
|
||||
|
@ -1267,6 +1292,7 @@ impl FilterResolved {
|
|||
FilterResolved::Eq(a, v, idx)
|
||||
}
|
||||
FilterComp::SelfUuid => panic!("Not possible to resolve SelfUuid in from_invalid!"),
|
||||
FilterComp::Invalid(attr) => FilterResolved::Invalid(attr),
|
||||
FilterComp::Cnt(a, v) => {
|
||||
let idx = idxmeta.contains(&(&a, &IndexType::SubString));
|
||||
let idx = NonZeroU8::new(idx as u8);
|
||||
|
@ -1340,6 +1366,7 @@ impl FilterResolved {
|
|||
| FilterComp::Stw(..)
|
||||
| FilterComp::Enw(..)
|
||||
| FilterComp::Pres(_)
|
||||
| FilterComp::Invalid(_)
|
||||
| FilterComp::LessThan(..) => true,
|
||||
}
|
||||
}
|
||||
|
@ -1435,6 +1462,7 @@ impl FilterResolved {
|
|||
FilterResolved::resolve_idx((*f).clone(), ev, idxmeta)
|
||||
.map(|fi| FilterResolved::AndNot(Box::new(fi), None))
|
||||
}
|
||||
FilterComp::Invalid(attr) => Some(FilterResolved::Invalid(attr)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1493,6 +1521,7 @@ impl FilterResolved {
|
|||
FilterResolved::resolve_no_idx((*f).clone(), ev)
|
||||
.map(|fi| FilterResolved::AndNot(Box::new(fi), None))
|
||||
}
|
||||
FilterComp::Invalid(attr) => Some(FilterResolved::Invalid(attr)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1632,6 +1661,8 @@ impl FilterResolved {
|
|||
| FilterResolved::And(_, sf)
|
||||
| FilterResolved::Inclusion(_, sf)
|
||||
| FilterResolved::AndNot(_, sf) => *sf,
|
||||
// We hard code 1 because there is no slope for an invlid filter
|
||||
FilterResolved::Invalid(_) => NonZeroU8::new(1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -205,9 +205,9 @@ impl LdapServer {
|
|||
// Map the Some(a,v) to ...?
|
||||
|
||||
let ext_filter = match (&sr.scope, req_dn) {
|
||||
// OneLevel and Child searches are veerrrryyy similar for us because child
|
||||
// OneLevel and Child searches are **very** similar for us because child
|
||||
// is a "subtree search excluding base". Because we don't have a tree structure at
|
||||
// all, this is the same as a onelevel (ald children of base excludeing base).
|
||||
// all, this is the same as a one level (all children of base excluding base).
|
||||
(LdapSearchScope::Children, Some(_r)) | (LdapSearchScope::OneLevel, Some(_r)) => {
|
||||
return Ok(vec![sr.gen_success()])
|
||||
}
|
||||
|
@ -239,7 +239,7 @@ impl LdapServer {
|
|||
let mut all_attrs = false;
|
||||
let mut all_op_attrs = false;
|
||||
|
||||
// TODO #67: limit the number of attributes here!
|
||||
// TODO #3406: limit the number of attributes here!
|
||||
if sr.attrs.is_empty() {
|
||||
// If [], then "all" attrs
|
||||
all_attrs = true;
|
||||
|
@ -1158,6 +1158,88 @@ mod tests {
|
|||
assert_eq!(r1.len(), r2.len());
|
||||
}
|
||||
|
||||
#[idm_test]
|
||||
async fn test_ldap_spn_search(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
|
||||
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
|
||||
|
||||
let usr_uuid = Uuid::new_v4();
|
||||
let usr_name = "panko";
|
||||
|
||||
// Setup person, group and application
|
||||
{
|
||||
let e1: Entry<EntryInit, EntryNew> = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname(usr_name)),
|
||||
(Attribute::Uuid, Value::Uuid(usr_uuid)),
|
||||
(Attribute::DisplayName, Value::new_utf8s(usr_name))
|
||||
);
|
||||
|
||||
let ct = duration_from_epoch_now();
|
||||
let mut server_txn = idms.proxy_write(ct).await.unwrap();
|
||||
assert!(server_txn
|
||||
.qs_write
|
||||
.internal_create(vec![e1])
|
||||
.and_then(|_| server_txn.commit())
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
// Setup the anonymous login
|
||||
let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
|
||||
assert_eq!(
|
||||
anon_t.effective_session,
|
||||
LdapSession::UnixBind(UUID_ANONYMOUS)
|
||||
);
|
||||
|
||||
// Searching a malformed spn shouldn't cause the query to fail
|
||||
let sr = SearchRequest {
|
||||
msgid: 1,
|
||||
base: format!("dc=example,dc=com"),
|
||||
scope: LdapSearchScope::Subtree,
|
||||
filter: LdapFilter::Or(vec![
|
||||
LdapFilter::Equality(Attribute::Name.to_string(), usr_name.to_string()),
|
||||
LdapFilter::Equality(Attribute::Spn.to_string(), usr_name.to_string()),
|
||||
]),
|
||||
attrs: vec!["*".to_string()],
|
||||
};
|
||||
|
||||
let result = ldaps
|
||||
.do_search(idms, &sr, &anon_t, Source::Internal)
|
||||
.await
|
||||
.map(|r| {
|
||||
r.into_iter()
|
||||
.filter(|r| matches!(r.op, LdapOp::SearchResultEntry(_)))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(!result.is_empty());
|
||||
|
||||
let sr = SearchRequest {
|
||||
msgid: 1,
|
||||
base: format!("dc=example,dc=com"),
|
||||
scope: LdapSearchScope::Subtree,
|
||||
filter: LdapFilter::And(vec![
|
||||
LdapFilter::Equality(Attribute::Name.to_string(), usr_name.to_string()),
|
||||
LdapFilter::Equality(Attribute::Spn.to_string(), usr_name.to_string()),
|
||||
]),
|
||||
attrs: vec!["*".to_string()],
|
||||
};
|
||||
|
||||
let empty_result = ldaps
|
||||
.do_search(idms, &sr, &anon_t, Source::Internal)
|
||||
.await
|
||||
.map(|r| {
|
||||
r.into_iter()
|
||||
.filter(|r| matches!(r.op, LdapOp::SearchResultEntry(_)))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(empty_result.is_empty());
|
||||
}
|
||||
|
||||
#[idm_test]
|
||||
async fn test_ldap_application_bind(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
|
||||
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
|
||||
|
|
|
@ -32,7 +32,7 @@ pub use kanidm_proto::oauth2::{
|
|||
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
|
||||
AccessTokenResponse, AuthorisationRequest, CodeChallengeMethod, ErrorResponse, GrantTypeReq,
|
||||
OAuth2RFC9068Token, OAuth2RFC9068TokenExtensions, Oauth2Rfc8414MetadataResponse,
|
||||
OidcDiscoveryResponse, PkceAlg, TokenRevokeRequest,
|
||||
OidcDiscoveryResponse, OidcWebfingerRel, OidcWebfingerResponse, PkceAlg, TokenRevokeRequest,
|
||||
};
|
||||
|
||||
use kanidm_proto::oauth2::{
|
||||
|
@ -248,24 +248,32 @@ impl AuthorisePermitSuccess {
|
|||
pub fn build_redirect_uri(&self) -> Url {
|
||||
let mut redirect_uri = self.redirect_uri.clone();
|
||||
|
||||
// Always clear query and fragment, regardless of the response mode
|
||||
redirect_uri.set_query(None);
|
||||
// Always clear the fragment per RFC
|
||||
redirect_uri.set_fragment(None);
|
||||
|
||||
// We can't set query pairs on fragments, only query.
|
||||
let mut uri_builder = url::form_urlencoded::Serializer::new(String::new());
|
||||
|
||||
uri_builder.append_pair("code", &self.code);
|
||||
|
||||
if let Some(state) = self.state.as_ref() {
|
||||
uri_builder.append_pair("state", state);
|
||||
};
|
||||
|
||||
let encoded = uri_builder.finish();
|
||||
|
||||
match self.response_mode {
|
||||
ResponseMode::Query => redirect_uri.set_query(Some(&encoded)),
|
||||
ResponseMode::Fragment => redirect_uri.set_fragment(Some(&encoded)),
|
||||
ResponseMode::Query => {
|
||||
redirect_uri
|
||||
.query_pairs_mut()
|
||||
.append_pair("code", &self.code);
|
||||
|
||||
if let Some(state) = self.state.as_ref() {
|
||||
redirect_uri.query_pairs_mut().append_pair("state", state);
|
||||
};
|
||||
}
|
||||
ResponseMode::Fragment => {
|
||||
redirect_uri.set_query(None);
|
||||
|
||||
// Per [the RFC](https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2), we can't set query pairs on fragment-containing redirects, only query ones.
|
||||
let mut uri_builder = url::form_urlencoded::Serializer::new(String::new());
|
||||
uri_builder.append_pair("code", &self.code);
|
||||
if let Some(state) = self.state.as_ref() {
|
||||
uri_builder.append_pair("state", state);
|
||||
};
|
||||
let encoded = uri_builder.finish();
|
||||
|
||||
redirect_uri.set_fragment(Some(&encoded))
|
||||
}
|
||||
}
|
||||
|
||||
redirect_uri
|
||||
|
@ -2742,6 +2750,44 @@ impl IdmServerProxyReadTransaction<'_> {
|
|||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub fn oauth2_openid_webfinger(
|
||||
&mut self,
|
||||
client_id: &str,
|
||||
resource_id: &str,
|
||||
) -> Result<OidcWebfingerResponse, OperationError> {
|
||||
let o2rs = self.oauth2rs.inner.rs_set.get(client_id).ok_or_else(|| {
|
||||
admin_warn!(
|
||||
"Invalid OAuth2 client_id (have you configured the OAuth2 resource server?)"
|
||||
);
|
||||
OperationError::NoMatchingEntries
|
||||
})?;
|
||||
|
||||
let Some(spn) = PartialValue::new_spn_s(resource_id) else {
|
||||
return Err(OperationError::NoMatchingEntries);
|
||||
};
|
||||
|
||||
// Ensure that the account exists.
|
||||
if !self
|
||||
.qs_read
|
||||
.internal_exists(Filter::new(f_eq(Attribute::Spn, spn)))?
|
||||
{
|
||||
return Err(OperationError::NoMatchingEntries);
|
||||
}
|
||||
|
||||
let issuer = o2rs.iss.clone();
|
||||
|
||||
Ok(OidcWebfingerResponse {
|
||||
// we set the subject to the resource_id to ensure we always send something valid back
|
||||
// but realistically this will be overwritten on at the API layer
|
||||
subject: resource_id.to_string(),
|
||||
links: vec![OidcWebfingerRel {
|
||||
rel: "http://openid.net/specs/connect/1.0/issuer".into(),
|
||||
href: issuer.into(),
|
||||
}],
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub fn oauth2_openid_publickey(&self, client_id: &str) -> Result<JwkKeySet, OperationError> {
|
||||
let o2rs = self.oauth2rs.inner.rs_set.get(client_id).ok_or_else(|| {
|
||||
|
@ -2982,7 +3028,7 @@ fn check_is_loopback(redirect_uri: &Url) -> bool {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::convert::TryFrom;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
@ -3106,7 +3152,7 @@ mod tests {
|
|||
),
|
||||
(
|
||||
Attribute::OAuth2RsOrigin,
|
||||
Value::new_url_s("https://portal.example.com").unwrap()
|
||||
Value::new_url_s("https://portal.example.com/?custom=foo").unwrap()
|
||||
),
|
||||
(
|
||||
Attribute::OAuth2RsOrigin,
|
||||
|
@ -3642,6 +3688,70 @@ mod tests {
|
|||
== Oauth2Error::InvalidOrigin
|
||||
);
|
||||
|
||||
// * invalid uri (doesn't match query params)
|
||||
let auth_req = AuthorisationRequest {
|
||||
response_type: ResponseType::Code,
|
||||
response_mode: None,
|
||||
client_id: "test_resource_server".to_string(),
|
||||
state: Some("123".to_string()),
|
||||
pkce_request: pkce_request.clone(),
|
||||
redirect_uri: Url::parse("https://portal.example.com/?custom=foo&too=many").unwrap(),
|
||||
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
|
||||
nonce: None,
|
||||
oidc_ext: Default::default(),
|
||||
max_age: None,
|
||||
unknown_keys: Default::default(),
|
||||
};
|
||||
|
||||
assert!(
|
||||
idms_prox_read
|
||||
.check_oauth2_authorisation(Some(&ident), &auth_req, ct)
|
||||
.unwrap_err()
|
||||
== Oauth2Error::InvalidOrigin
|
||||
);
|
||||
|
||||
let auth_req = AuthorisationRequest {
|
||||
response_type: ResponseType::Code,
|
||||
response_mode: None,
|
||||
client_id: "test_resource_server".to_string(),
|
||||
state: Some("123".to_string()),
|
||||
pkce_request: pkce_request.clone(),
|
||||
redirect_uri: Url::parse("https://portal.example.com").unwrap(),
|
||||
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
|
||||
nonce: None,
|
||||
oidc_ext: Default::default(),
|
||||
max_age: None,
|
||||
unknown_keys: Default::default(),
|
||||
};
|
||||
|
||||
assert!(
|
||||
idms_prox_read
|
||||
.check_oauth2_authorisation(Some(&ident), &auth_req, ct)
|
||||
.unwrap_err()
|
||||
== Oauth2Error::InvalidOrigin
|
||||
);
|
||||
|
||||
let auth_req = AuthorisationRequest {
|
||||
response_type: ResponseType::Code,
|
||||
response_mode: None,
|
||||
client_id: "test_resource_server".to_string(),
|
||||
state: Some("123".to_string()),
|
||||
pkce_request: pkce_request.clone(),
|
||||
redirect_uri: Url::parse("https://portal.example.com/?wrong=queryparam").unwrap(),
|
||||
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
|
||||
nonce: None,
|
||||
oidc_ext: Default::default(),
|
||||
max_age: None,
|
||||
unknown_keys: Default::default(),
|
||||
};
|
||||
|
||||
assert!(
|
||||
idms_prox_read
|
||||
.check_oauth2_authorisation(Some(&ident), &auth_req, ct)
|
||||
.unwrap_err()
|
||||
== Oauth2Error::InvalidOrigin
|
||||
);
|
||||
|
||||
// Not Authenticated
|
||||
let auth_req = AuthorisationRequest {
|
||||
response_type: ResponseType::Code,
|
||||
|
@ -4003,6 +4113,8 @@ mod tests {
|
|||
// == Setup the authorisation request
|
||||
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||
|
||||
let redirect_uri = Url::parse("https://portal.example.com/?custom=foo").unwrap();
|
||||
|
||||
let auth_req = AuthorisationRequest {
|
||||
response_type: ResponseType::Code,
|
||||
response_mode: None,
|
||||
|
@ -4012,7 +4124,7 @@ mod tests {
|
|||
code_challenge: code_challenge.clone(),
|
||||
code_challenge_method: CodeChallengeMethod::S256,
|
||||
}),
|
||||
redirect_uri: Url::parse("https://portal.example.com").unwrap(),
|
||||
redirect_uri: redirect_uri.clone(),
|
||||
scope: btreeset![OAUTH2_SCOPE_GROUPS.to_string()],
|
||||
nonce: Some("abcdef".to_string()),
|
||||
oidc_ext: Default::default(),
|
||||
|
@ -4042,12 +4154,22 @@ mod tests {
|
|||
// Check we are reflecting the CSRF properly.
|
||||
assert_eq!(permit_success.state.as_deref(), None);
|
||||
|
||||
// Assert we followed the redirect uri including the query elements
|
||||
// we have in the url.
|
||||
let permit_redirect_uri = permit_success.build_redirect_uri();
|
||||
|
||||
assert_eq!(permit_redirect_uri.origin(), redirect_uri.origin());
|
||||
assert_eq!(permit_redirect_uri.path(), redirect_uri.path());
|
||||
let query = BTreeMap::from_iter(permit_redirect_uri.query_pairs().into_owned());
|
||||
// Assert the query pair wasn't changed
|
||||
assert_eq!(query.get("custom").map(|s| s.as_str()), Some("foo"));
|
||||
|
||||
// == Submit the token exchange code.
|
||||
// ⚠️ This is where we submit a different origin!
|
||||
let token_req = AccessTokenRequest {
|
||||
grant_type: GrantTypeReq::AuthorizationCode {
|
||||
code: permit_success.code,
|
||||
redirect_uri: Url::parse("https://portal.example.com").unwrap(),
|
||||
redirect_uri,
|
||||
// From the first step.
|
||||
code_verifier: code_verifier.clone(),
|
||||
},
|
||||
|
@ -5434,6 +5556,34 @@ mod tests {
|
|||
.expect("Oauth2 authorisation failed");
|
||||
}
|
||||
|
||||
#[idm_test]
|
||||
async fn test_idm_oauth2_webfinger(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
|
||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||
let (_secret, _uat, _ident, _) =
|
||||
setup_oauth2_resource_server_basic(idms, ct, true, false, true).await;
|
||||
let mut idms_prox_read = idms.proxy_read().await.unwrap();
|
||||
|
||||
let user = "testperson1@example.com";
|
||||
|
||||
let webfinger = idms_prox_read
|
||||
.oauth2_openid_webfinger("test_resource_server", user)
|
||||
.expect("Failed to get webfinger");
|
||||
|
||||
assert_eq!(webfinger.subject, user);
|
||||
assert_eq!(webfinger.links.len(), 1);
|
||||
|
||||
let link = &webfinger.links[0];
|
||||
assert_eq!(link.rel, "http://openid.net/specs/connect/1.0/issuer");
|
||||
assert_eq!(
|
||||
link.href,
|
||||
"https://idm.example.com/oauth2/openid/test_resource_server"
|
||||
);
|
||||
|
||||
let failed_webfinger = idms_prox_read
|
||||
.oauth2_openid_webfinger("test_resource_server", "someone@another.domain");
|
||||
assert!(failed_webfinger.is_err());
|
||||
}
|
||||
|
||||
#[idm_test]
|
||||
async fn test_idm_oauth2_openid_legacy_crypto(
|
||||
idms: &IdmServer,
|
||||
|
|
|
@ -5,7 +5,7 @@ use base64::{
|
|||
Engine as _,
|
||||
};
|
||||
|
||||
use compact_jwt::{Jws, JwsCompact, JwsEs256Signer, JwsSigner};
|
||||
use compact_jwt::{Jws, JwsCompact};
|
||||
use kanidm_proto::internal::{ApiTokenPurpose, ScimSyncToken};
|
||||
use kanidm_proto::scim_v1::*;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
@ -25,7 +25,6 @@ pub(crate) struct SyncAccount {
|
|||
pub name: String,
|
||||
pub uuid: Uuid,
|
||||
pub sync_tokens: BTreeMap<Uuid, ApiToken>,
|
||||
pub jws_key: Option<JwsEs256Signer>,
|
||||
}
|
||||
|
||||
macro_rules! try_from_entry {
|
||||
|
@ -40,15 +39,6 @@ macro_rules! try_from_entry {
|
|||
.map(|s| s.to_string())
|
||||
.ok_or(OperationError::MissingAttribute(Attribute::Name))?;
|
||||
|
||||
let jws_key = $value
|
||||
.get_ava_single_jws_key_es256(Attribute::JwsEs256PrivateKey)
|
||||
.cloned()
|
||||
.map(|jws_key| {
|
||||
jws_key
|
||||
.set_sign_option_embed_jwk(true)
|
||||
.set_sign_option_legacy_kid(true)
|
||||
});
|
||||
|
||||
let sync_tokens = $value
|
||||
.get_ava_as_apitoken_map(Attribute::SyncTokenSession)
|
||||
.cloned()
|
||||
|
@ -60,7 +50,6 @@ macro_rules! try_from_entry {
|
|||
name,
|
||||
uuid,
|
||||
sync_tokens,
|
||||
jws_key,
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
@ -123,16 +112,6 @@ impl IdmServerProxyWriteTransaction<'_> {
|
|||
gte: &GenerateScimSyncTokenEvent,
|
||||
ct: Duration,
|
||||
) -> Result<JwsCompact, OperationError> {
|
||||
// Get the target signing key.
|
||||
let sync_account = self
|
||||
.qs_write
|
||||
.internal_search_uuid(gte.target)
|
||||
.and_then(|entry| SyncAccount::try_from_entry_rw(&entry))
|
||||
.map_err(|e| {
|
||||
admin_error!(?e, "Failed to search service account");
|
||||
e
|
||||
})?;
|
||||
|
||||
let session_id = Uuid::new_v4();
|
||||
let issued_at = time::OffsetDateTime::UNIX_EPOCH + ct;
|
||||
|
||||
|
@ -185,25 +164,9 @@ impl IdmServerProxyWriteTransaction<'_> {
|
|||
})?;
|
||||
|
||||
// The modify succeeded and was allowed, now sign the token for return.
|
||||
if self.qs_write.get_domain_version() < DOMAIN_LEVEL_6 {
|
||||
sync_account
|
||||
.jws_key
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
admin_error!("Unable to sign sync token, no sync keys available");
|
||||
OperationError::CryptographyError
|
||||
})
|
||||
.and_then(|jws_key| {
|
||||
jws_key.sign(&token).map_err(|err| {
|
||||
admin_error!(?err, "Unable to sign sync token");
|
||||
OperationError::CryptographyError
|
||||
})
|
||||
})
|
||||
} else {
|
||||
self.qs_write
|
||||
.get_domain_key_object_handle()?
|
||||
.jws_es256_sign(&token, ct)
|
||||
}
|
||||
self.qs_write
|
||||
.get_domain_key_object_handle()?
|
||||
.jws_es256_sign(&token, ct)
|
||||
// Done!
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use compact_jwt::{Jws, JwsCompact, JwsEs256Signer, JwsSigner};
|
||||
use compact_jwt::{Jws, JwsCompact};
|
||||
use kanidm_proto::internal::ApiToken as ProtoApiToken;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
|
@ -23,15 +23,6 @@ macro_rules! try_from_entry {
|
|||
));
|
||||
}
|
||||
|
||||
let jws_key = $value
|
||||
.get_ava_single_jws_key_es256(Attribute::JwsEs256PrivateKey)
|
||||
.cloned()
|
||||
.map(|jws_key| {
|
||||
jws_key
|
||||
.set_sign_option_embed_jwk(true)
|
||||
.set_sign_option_legacy_kid(true)
|
||||
});
|
||||
|
||||
let api_tokens = $value
|
||||
.get_ava_as_apitoken_map(Attribute::ApiTokenSession)
|
||||
.cloned()
|
||||
|
@ -48,7 +39,6 @@ macro_rules! try_from_entry {
|
|||
valid_from,
|
||||
expire,
|
||||
api_tokens,
|
||||
jws_key,
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
@ -60,8 +50,6 @@ pub struct ServiceAccount {
|
|||
pub expire: Option<OffsetDateTime>,
|
||||
|
||||
pub api_tokens: BTreeMap<Uuid, ApiToken>,
|
||||
|
||||
pub jws_key: Option<JwsEs256Signer>,
|
||||
}
|
||||
|
||||
impl ServiceAccount {
|
||||
|
@ -253,25 +241,9 @@ impl IdmServerProxyWriteTransaction<'_> {
|
|||
err
|
||||
})?;
|
||||
|
||||
if self.qs_write.get_domain_version() < DOMAIN_LEVEL_6 {
|
||||
service_account
|
||||
.jws_key
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
admin_error!("Unable to sign sync token, no sync keys available");
|
||||
OperationError::CryptographyError
|
||||
})
|
||||
.and_then(|jws_key| {
|
||||
jws_key.sign(&token).map_err(|err| {
|
||||
admin_error!(?err, "Unable to sign sync token");
|
||||
OperationError::CryptographyError
|
||||
})
|
||||
})
|
||||
} else {
|
||||
self.qs_write
|
||||
.get_domain_key_object_handle()?
|
||||
.jws_es256_sign(&token, ct)
|
||||
}
|
||||
self.qs_write
|
||||
.get_domain_key_object_handle()?
|
||||
.jws_es256_sign(&token, ct)
|
||||
}
|
||||
|
||||
pub fn service_account_destroy_api_token(
|
||||
|
|
|
@ -89,8 +89,8 @@ pub mod prelude {
|
|||
};
|
||||
pub use crate::event::{CreateEvent, DeleteEvent, ExistsEvent, ModifyEvent, SearchEvent};
|
||||
pub use crate::filter::{
|
||||
f_and, f_andnot, f_eq, f_id, f_inc, f_lt, f_or, f_pres, f_self, f_spn_name, f_sub, Filter,
|
||||
FilterInvalid, FilterValid, FC,
|
||||
f_and, f_andnot, f_eq, f_id, f_inc, f_invalid, f_lt, f_or, f_pres, f_self, f_spn_name,
|
||||
f_sub, Filter, FilterInvalid, FilterValid, FC,
|
||||
};
|
||||
pub use crate::idm::server::{IdmServer, IdmServerAudit, IdmServerDelayed};
|
||||
pub use crate::idm::{ClientAuthInfo, ClientCertInfo};
|
||||
|
|
|
@ -7,8 +7,6 @@
|
|||
use std::iter::once;
|
||||
use std::sync::Arc;
|
||||
|
||||
use compact_jwt::JwsEs256Signer;
|
||||
use rand::prelude::*;
|
||||
use regex::Regex;
|
||||
use tracing::trace;
|
||||
|
||||
|
@ -61,13 +59,6 @@ impl Plugin for Domain {
|
|||
}
|
||||
}
|
||||
|
||||
fn generate_domain_cookie_key() -> Value {
|
||||
let mut key = [0; 64];
|
||||
let mut rng = StdRng::from_entropy();
|
||||
rng.fill(&mut key);
|
||||
Value::new_privatebinary(&key)
|
||||
}
|
||||
|
||||
impl Domain {
|
||||
/// Generates the cookie key for the domain.
|
||||
fn modify_inner<T: Clone + std::fmt::Debug>(
|
||||
|
@ -79,11 +70,14 @@ impl Domain {
|
|||
&& e.attribute_equality(Attribute::Uuid, &PVUUID_DOMAIN_INFO)
|
||||
{
|
||||
// Validate the domain ldap basedn syntax.
|
||||
if let Some(basedn) = e
|
||||
.get_ava_single_iutf8(Attribute::DomainLdapBasedn) {
|
||||
|
||||
if let Some(basedn) = e.get_ava_single_iutf8(Attribute::DomainLdapBasedn) {
|
||||
if !DOMAIN_LDAP_BASEDN_RE.is_match(basedn) {
|
||||
error!("Invalid {} '{}'. Must pass regex \"{}\"", Attribute::DomainLdapBasedn,basedn, *DOMAIN_LDAP_BASEDN_RE);
|
||||
error!(
|
||||
"Invalid {} '{}'. Must pass regex \"{}\"",
|
||||
Attribute::DomainLdapBasedn,
|
||||
basedn,
|
||||
*DOMAIN_LDAP_BASEDN_RE
|
||||
);
|
||||
return Err(OperationError::InvalidState);
|
||||
}
|
||||
}
|
||||
|
@ -109,39 +103,26 @@ impl Domain {
|
|||
debug!("plugin_domain: NOT Applying domain version transform");
|
||||
};
|
||||
|
||||
// create the domain_display_name if it's missing
|
||||
if !e.attribute_pres(Attribute::DomainDisplayName) {
|
||||
let domain_display_name = Value::new_utf8(format!("Kanidm {}", qs.get_domain_name()));
|
||||
security_info!("plugin_domain: setting default domain_display_name to {:?}", domain_display_name);
|
||||
// create the domain_display_name if it's missing. This was the behaviour in versions
|
||||
// prior to DL10. Rather than checking the domain version itself, the issue is we
|
||||
// have to check the min remigration level. This is because during a server setup
|
||||
// we start from the MIN remigration level and work up, and the domain version == 0.
|
||||
//
|
||||
// So effectively we only skip setting this value after we know that we are at DL12
|
||||
// since we could never go back to anything lower than 10 at that point.
|
||||
if DOMAIN_MIN_REMIGRATION_LEVEL < DOMAIN_LEVEL_10
|
||||
&& !e.attribute_pres(Attribute::DomainDisplayName)
|
||||
{
|
||||
let domain_display_name =
|
||||
Value::new_utf8(format!("Kanidm {}", qs.get_domain_name()));
|
||||
security_info!(
|
||||
"plugin_domain: setting default domain_display_name to {:?}",
|
||||
domain_display_name
|
||||
);
|
||||
|
||||
e.set_ava(&Attribute::DomainDisplayName, once(domain_display_name));
|
||||
}
|
||||
|
||||
if qs.get_domain_version() < DOMAIN_LEVEL_6 && !e.attribute_pres(Attribute::FernetPrivateKeyStr) {
|
||||
security_info!("regenerating domain token encryption key");
|
||||
let k = fernet::Fernet::generate_key();
|
||||
let v = Value::new_secret_str(&k);
|
||||
e.add_ava(Attribute::FernetPrivateKeyStr, v);
|
||||
}
|
||||
|
||||
if qs.get_domain_version() < DOMAIN_LEVEL_6 && !e.attribute_pres(Attribute::Es256PrivateKeyDer) {
|
||||
security_info!("regenerating domain es256 private key");
|
||||
let der = JwsEs256Signer::generate_es256()
|
||||
.and_then(|jws| jws.private_key_to_der())
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Unable to generate ES256 JwsSigner private key");
|
||||
OperationError::CryptographyError
|
||||
})?;
|
||||
let v = Value::new_privatebinary(&der);
|
||||
e.add_ava(Attribute::Es256PrivateKeyDer, v);
|
||||
}
|
||||
|
||||
if qs.get_domain_version() < DOMAIN_LEVEL_6 && !e.attribute_pres(Attribute::PrivateCookieKey) {
|
||||
security_info!("regenerating domain cookie key");
|
||||
e.add_ava(Attribute::PrivateCookieKey, generate_domain_cookie_key());
|
||||
}
|
||||
|
||||
trace!(?e);
|
||||
Ok(())
|
||||
} else {
|
||||
Ok(())
|
||||
|
|
|
@ -62,10 +62,7 @@ pub const GID_UNUSED_D_MAX: u32 = 0x7fff_ffff;
|
|||
|
||||
pub struct GidNumber {}
|
||||
|
||||
fn apply_gidnumber<T: Clone>(
|
||||
e: &mut Entry<EntryInvalid, T>,
|
||||
domain_version: DomainVersion,
|
||||
) -> Result<(), OperationError> {
|
||||
fn apply_gidnumber<T: Clone>(e: &mut Entry<EntryInvalid, T>) -> Result<(), OperationError> {
|
||||
if (e.attribute_equality(Attribute::Class, &EntryClass::PosixGroup.into())
|
||||
|| e.attribute_equality(Attribute::Class, &EntryClass::PosixAccount.into()))
|
||||
&& !e.attribute_pres(Attribute::GidNumber)
|
||||
|
@ -89,48 +86,33 @@ fn apply_gidnumber<T: Clone>(
|
|||
e.set_ava(&Attribute::GidNumber, once(gid_v));
|
||||
Ok(())
|
||||
} else if let Some(gid) = e.get_ava_single_uint32(Attribute::GidNumber) {
|
||||
if domain_version <= DOMAIN_LEVEL_6 {
|
||||
if gid < GID_REGULAR_USER_MIN {
|
||||
error!(
|
||||
"Requested GID ({}) overlaps a system range. Allowed ranges are {} to {}, {} to {} and {} to {}",
|
||||
gid,
|
||||
GID_REGULAR_USER_MIN, GID_REGULAR_USER_MAX,
|
||||
GID_UNUSED_C_MIN, GID_UNUSED_C_MAX,
|
||||
GID_UNUSED_D_MIN, GID_UNUSED_D_MAX
|
||||
);
|
||||
Err(OperationError::PL0001GidOverlapsSystemRange)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
// If they provided us with a gid number, ensure it's in a safe range.
|
||||
if (GID_REGULAR_USER_MIN..=GID_REGULAR_USER_MAX).contains(&gid)
|
||||
|| (GID_UNUSED_A_MIN..=GID_UNUSED_A_MAX).contains(&gid)
|
||||
|| (GID_UNUSED_B_MIN..= GID_UNUSED_B_MAX).contains(&gid)
|
||||
|| (GID_UNUSED_C_MIN..=GID_UNUSED_C_MAX).contains(&gid)
|
||||
// We won't ever generate an id in the nspawn range, but we do secretly allow
|
||||
// it to be set for compatibility with services like freeipa or openldap. TBH
|
||||
// most people don't even use systemd nspawn anyway ...
|
||||
//
|
||||
// I made this design choice to avoid a tunable that may confuse people to
|
||||
// its purpose. This way things "just work" for imports and existing systems
|
||||
// but we do the right thing in the future.
|
||||
|| (GID_NSPAWN_MIN..=GID_NSPAWN_MAX).contains(&gid)
|
||||
|| (GID_UNUSED_D_MIN..=GID_UNUSED_D_MAX).contains(&gid)
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
// If they provided us with a gid number, ensure it's in a safe range.
|
||||
if (GID_REGULAR_USER_MIN..=GID_REGULAR_USER_MAX).contains(&gid)
|
||||
|| (GID_UNUSED_A_MIN..=GID_UNUSED_A_MAX).contains(&gid)
|
||||
|| (GID_UNUSED_B_MIN..= GID_UNUSED_B_MAX).contains(&gid)
|
||||
|| (GID_UNUSED_C_MIN..=GID_UNUSED_C_MAX).contains(&gid)
|
||||
// We won't ever generate an id in the nspawn range, but we do secretly allow
|
||||
// it to be set for compatibility with services like freeipa or openldap. TBH
|
||||
// most people don't even use systemd nspawn anyway ...
|
||||
//
|
||||
// I made this design choice to avoid a tunable that may confuse people to
|
||||
// its purpose. This way things "just work" for imports and existing systems
|
||||
// but we do the right thing in the future.
|
||||
|| (GID_NSPAWN_MIN..=GID_NSPAWN_MAX).contains(&gid)
|
||||
|| (GID_UNUSED_D_MIN..=GID_UNUSED_D_MAX).contains(&gid)
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
// Note that here we don't advertise that we allow the nspawn range to be set, even
|
||||
// though we do allow it.
|
||||
error!(
|
||||
"Requested GID ({}) overlaps a system range. Allowed ranges are {} to {}, {} to {} and {} to {}",
|
||||
gid,
|
||||
GID_REGULAR_USER_MIN, GID_REGULAR_USER_MAX,
|
||||
GID_UNUSED_C_MIN, GID_UNUSED_C_MAX,
|
||||
GID_UNUSED_D_MIN, GID_UNUSED_D_MAX
|
||||
);
|
||||
Err(OperationError::PL0001GidOverlapsSystemRange)
|
||||
}
|
||||
// Note that here we don't advertise that we allow the nspawn range to be set, even
|
||||
// though we do allow it.
|
||||
error!(
|
||||
"Requested GID ({}) overlaps a system range. Allowed ranges are {} to {}, {} to {} and {} to {}",
|
||||
gid,
|
||||
GID_REGULAR_USER_MIN, GID_REGULAR_USER_MAX,
|
||||
GID_UNUSED_C_MIN, GID_UNUSED_C_MAX,
|
||||
GID_UNUSED_D_MIN, GID_UNUSED_D_MAX
|
||||
);
|
||||
Err(OperationError::PL0001GidOverlapsSystemRange)
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
|
@ -144,37 +126,31 @@ impl Plugin for GidNumber {
|
|||
|
||||
#[instrument(level = "debug", name = "gidnumber_pre_create_transform", skip_all)]
|
||||
fn pre_create_transform(
|
||||
qs: &mut QueryServerWriteTransaction,
|
||||
_qs: &mut QueryServerWriteTransaction,
|
||||
cand: &mut Vec<Entry<EntryInvalid, EntryNew>>,
|
||||
_ce: &CreateEvent,
|
||||
) -> Result<(), OperationError> {
|
||||
let dv = qs.get_domain_version();
|
||||
cand.iter_mut()
|
||||
.try_for_each(|cand| apply_gidnumber(cand, dv))
|
||||
cand.iter_mut().try_for_each(apply_gidnumber)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", name = "gidnumber_pre_modify", skip_all)]
|
||||
fn pre_modify(
|
||||
qs: &mut QueryServerWriteTransaction,
|
||||
_qs: &mut QueryServerWriteTransaction,
|
||||
_pre_cand: &[Arc<EntrySealedCommitted>],
|
||||
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
|
||||
_me: &ModifyEvent,
|
||||
) -> Result<(), OperationError> {
|
||||
let dv = qs.get_domain_version();
|
||||
cand.iter_mut()
|
||||
.try_for_each(|cand| apply_gidnumber(cand, dv))
|
||||
cand.iter_mut().try_for_each(apply_gidnumber)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", name = "gidnumber_pre_batch_modify", skip_all)]
|
||||
fn pre_batch_modify(
|
||||
qs: &mut QueryServerWriteTransaction,
|
||||
_qs: &mut QueryServerWriteTransaction,
|
||||
_pre_cand: &[Arc<EntrySealedCommitted>],
|
||||
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
|
||||
_me: &BatchModifyEvent,
|
||||
) -> Result<(), OperationError> {
|
||||
let dv = qs.get_domain_version();
|
||||
cand.iter_mut()
|
||||
.try_for_each(|cand| apply_gidnumber(cand, dv))
|
||||
cand.iter_mut().try_for_each(apply_gidnumber)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,9 +162,7 @@ mod tests {
|
|||
};
|
||||
use crate::prelude::*;
|
||||
|
||||
use kanidm_proto::internal::DomainUpgradeCheckStatus as ProtoDomainUpgradeCheckStatus;
|
||||
|
||||
#[qs_test(domain_level=DOMAIN_LEVEL_7)]
|
||||
#[qs_test]
|
||||
async fn test_gidnumber_generate(server: &QueryServer) {
|
||||
let mut server_txn = server.write(duration_from_epoch_now()).await.expect("txn");
|
||||
|
||||
|
@ -423,85 +397,4 @@ mod tests {
|
|||
|
||||
assert!(server_txn.commit().is_ok());
|
||||
}
|
||||
|
||||
#[qs_test(domain_level=DOMAIN_LEVEL_6)]
|
||||
async fn test_gidnumber_domain_level_6(server: &QueryServer) {
|
||||
let mut server_txn = server.write(duration_from_epoch_now()).await.expect("txn");
|
||||
|
||||
// This will be INVALID in DL 7 but it's allowed for DL6
|
||||
let user_a_uuid = uuid!("d90fb0cb-6785-4f36-94cb-e364d9c13255");
|
||||
{
|
||||
let op_result = server_txn.internal_create(vec![entry_init!(
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::PosixAccount.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson_2")),
|
||||
(Attribute::Uuid, Value::Uuid(user_a_uuid)),
|
||||
// NOTE HERE: We do GID_UNUSED_A_MIN minus 1 which isn't accepted
|
||||
// on DL7
|
||||
(Attribute::GidNumber, Value::Uint32(GID_UNUSED_A_MIN - 1)),
|
||||
(Attribute::Description, Value::new_utf8s("testperson")),
|
||||
(Attribute::DisplayName, Value::new_utf8s("testperson"))
|
||||
)]);
|
||||
|
||||
assert!(op_result.is_ok());
|
||||
|
||||
let user_a = server_txn
|
||||
.internal_search_uuid(user_a_uuid)
|
||||
.expect("Unable to access user");
|
||||
|
||||
let user_a_uid = user_a
|
||||
.get_ava_single_uint32(Attribute::GidNumber)
|
||||
.expect("gidnumber not present on account");
|
||||
|
||||
assert_eq!(user_a_uid, GID_UNUSED_A_MIN - 1);
|
||||
}
|
||||
|
||||
assert!(server_txn.commit().is_ok());
|
||||
|
||||
// Now, do the DL6 upgrade check - will FAIL because the above user has an invalid ID.
|
||||
let mut server_txn = server.read().await.unwrap();
|
||||
|
||||
let check_item = server_txn
|
||||
.domain_upgrade_check_6_to_7_gidnumber()
|
||||
.expect("Failed to perform migration check.");
|
||||
|
||||
assert_eq!(
|
||||
check_item.status,
|
||||
ProtoDomainUpgradeCheckStatus::Fail6To7Gidnumber
|
||||
);
|
||||
|
||||
drop(server_txn);
|
||||
|
||||
let mut server_txn = server.write(duration_from_epoch_now()).await.expect("txn");
|
||||
|
||||
// Test rejection of important gid values.
|
||||
let user_b_uuid = uuid!("33afc396-2434-47e5-b143-05176148b50e");
|
||||
// Test that an entry when modified to have posix attributes, if a gidnumber
|
||||
// is provided then it is respected.
|
||||
{
|
||||
let op_result = server_txn.internal_create(vec![entry_init!(
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson_6")),
|
||||
(Attribute::Uuid, Value::Uuid(user_b_uuid)),
|
||||
(Attribute::Description, Value::new_utf8s("testperson")),
|
||||
(Attribute::DisplayName, Value::new_utf8s("testperson"))
|
||||
)]);
|
||||
|
||||
assert!(op_result.is_ok());
|
||||
|
||||
for id in [0, 500, GID_REGULAR_USER_MIN - 1] {
|
||||
let modlist = modlist!([
|
||||
m_pres(Attribute::Class, &EntryClass::PosixAccount.to_value()),
|
||||
m_pres(Attribute::GidNumber, &Value::Uint32(id))
|
||||
]);
|
||||
let op_result = server_txn.internal_modify_uuid(user_b_uuid, &modlist);
|
||||
|
||||
trace!(?id);
|
||||
assert_eq!(op_result, Err(OperationError::PL0001GidOverlapsSystemRange));
|
||||
}
|
||||
}
|
||||
|
||||
assert!(server_txn.commit().is_ok());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ impl Plugin for JwsKeygen {
|
|||
|
||||
impl JwsKeygen {
|
||||
fn modify_inner<T: Clone>(
|
||||
qs: &mut QueryServerWriteTransaction,
|
||||
_qs: &mut QueryServerWriteTransaction,
|
||||
cand: &mut [Entry<EntryInvalid, T>],
|
||||
) -> Result<(), OperationError> {
|
||||
cand.iter_mut().try_for_each(|e| {
|
||||
|
@ -88,20 +88,6 @@ impl JwsKeygen {
|
|||
}
|
||||
}
|
||||
|
||||
if qs.get_domain_version() < DOMAIN_LEVEL_6 &&
|
||||
(e.attribute_equality(Attribute::Class, &EntryClass::ServiceAccount.into()) ||
|
||||
e.attribute_equality(Attribute::Class, &EntryClass::SyncAccount.into())) &&
|
||||
!e.attribute_pres(Attribute::JwsEs256PrivateKey) {
|
||||
security_info!("regenerating jws es256 private key");
|
||||
let jwssigner = JwsEs256Signer::generate_es256()
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Unable to generate ES256 JwsSigner private key");
|
||||
OperationError::CryptographyError
|
||||
})?;
|
||||
let v = Value::JwsKeyEs256(jwssigner);
|
||||
e.add_ava(Attribute::JwsEs256PrivateKey, v);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -158,7 +158,7 @@ impl QueryServer {
|
|||
|
||||
// If we are new enough to support patches, and we are lower than the target patch level
|
||||
// then a reload will be applied after we raise the patch level.
|
||||
if domain_target_level >= DOMAIN_LEVEL_7 && domain_patch_level < DOMAIN_TGT_PATCH_LEVEL {
|
||||
if domain_patch_level < DOMAIN_TGT_PATCH_LEVEL {
|
||||
write_txn
|
||||
.internal_modify_uuid(
|
||||
UUID_DOMAIN_INFO,
|
||||
|
@ -294,346 +294,6 @@ impl QueryServerWriteTransaction<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Migration domain level 6 to 7
|
||||
#[instrument(level = "info", skip_all)]
|
||||
pub(crate) fn migrate_domain_6_to_7(&mut self) -> Result<(), OperationError> {
|
||||
if !cfg!(test) && DOMAIN_MAX_LEVEL < DOMAIN_LEVEL_7 {
|
||||
error!("Unable to raise domain level from 6 to 7.");
|
||||
return Err(OperationError::MG0004DomainLevelInDevelopment);
|
||||
}
|
||||
|
||||
// ============== Apply constraints ===============
|
||||
|
||||
// Due to changes in gidnumber allocation, in the *extremely* unlikely
|
||||
// case that a user's ID was generated outside the valid range, we re-request
|
||||
// the creation of their gid number to proceed.
|
||||
let filter = filter!(f_and!([
|
||||
f_or!([
|
||||
f_eq(Attribute::Class, EntryClass::PosixAccount.into()),
|
||||
f_eq(Attribute::Class, EntryClass::PosixGroup.into())
|
||||
]),
|
||||
// This logic gets a bit messy but it would be:
|
||||
// If ! (
|
||||
// (GID_REGULAR_USER_MIN < value < GID_REGULAR_USER_MAX) ||
|
||||
// (GID_UNUSED_A_MIN < value < GID_UNUSED_A_MAX) ||
|
||||
// (GID_UNUSED_B_MIN < value < GID_UNUSED_B_MAX) ||
|
||||
// (GID_UNUSED_C_MIN < value < GID_UNUSED_D_MAX)
|
||||
// )
|
||||
f_andnot(f_or!([
|
||||
f_and!([
|
||||
// The gid value must be less than GID_REGULAR_USER_MAX
|
||||
f_lt(
|
||||
Attribute::GidNumber,
|
||||
PartialValue::Uint32(crate::plugins::gidnumber::GID_REGULAR_USER_MAX)
|
||||
),
|
||||
// This bit of mental gymnastics is "greater than".
|
||||
// The gid value must not be less than USER_MIN
|
||||
f_andnot(f_lt(
|
||||
Attribute::GidNumber,
|
||||
PartialValue::Uint32(crate::plugins::gidnumber::GID_REGULAR_USER_MIN)
|
||||
))
|
||||
]),
|
||||
f_and!([
|
||||
f_lt(
|
||||
Attribute::GidNumber,
|
||||
PartialValue::Uint32(crate::plugins::gidnumber::GID_UNUSED_A_MAX)
|
||||
),
|
||||
f_andnot(f_lt(
|
||||
Attribute::GidNumber,
|
||||
PartialValue::Uint32(crate::plugins::gidnumber::GID_UNUSED_A_MIN)
|
||||
))
|
||||
]),
|
||||
f_and!([
|
||||
f_lt(
|
||||
Attribute::GidNumber,
|
||||
PartialValue::Uint32(crate::plugins::gidnumber::GID_UNUSED_B_MAX)
|
||||
),
|
||||
f_andnot(f_lt(
|
||||
Attribute::GidNumber,
|
||||
PartialValue::Uint32(crate::plugins::gidnumber::GID_UNUSED_B_MIN)
|
||||
))
|
||||
]),
|
||||
// If both of these conditions are true we get:
|
||||
// C_MIN < value < D_MAX, which the outer and-not inverts.
|
||||
f_and!([
|
||||
// The gid value must be less than GID_UNUSED_D_MAX
|
||||
f_lt(
|
||||
Attribute::GidNumber,
|
||||
PartialValue::Uint32(crate::plugins::gidnumber::GID_UNUSED_D_MAX)
|
||||
),
|
||||
// This bit of mental gymnastics is "greater than".
|
||||
// The gid value must not be less than C_MIN
|
||||
f_andnot(f_lt(
|
||||
Attribute::GidNumber,
|
||||
PartialValue::Uint32(crate::plugins::gidnumber::GID_UNUSED_C_MIN)
|
||||
))
|
||||
]),
|
||||
]))
|
||||
]));
|
||||
|
||||
let results = self.internal_search(filter).map_err(|err| {
|
||||
error!(?err, "migrate_domain_6_to_7 -> Error");
|
||||
err
|
||||
})?;
|
||||
|
||||
if !results.is_empty() {
|
||||
error!("Unable to proceed. Not all entries meet gid/uid constraints.");
|
||||
for entry in results {
|
||||
error!(gid_invalid = ?entry.get_display_id());
|
||||
}
|
||||
return Err(OperationError::MG0005GidConstraintsNotMet);
|
||||
}
|
||||
|
||||
// =========== Apply changes ==============
|
||||
|
||||
// For each oauth2 client, if it is missing a landing page then we clone the origin
|
||||
// into landing. This is because previously we implied the landing to be origin if
|
||||
// unset, but now landing is the primary url and implies an origin.
|
||||
let filter = filter!(f_and!([
|
||||
f_eq(Attribute::Class, EntryClass::OAuth2ResourceServer.into()),
|
||||
f_pres(Attribute::OAuth2RsOrigin),
|
||||
f_andnot(f_pres(Attribute::OAuth2RsOriginLanding)),
|
||||
]));
|
||||
|
||||
let pre_candidates = self.internal_search(filter).map_err(|err| {
|
||||
error!(?err, "migrate_domain_6_to_7 internal search failure");
|
||||
err
|
||||
})?;
|
||||
|
||||
let modset: Vec<_> = pre_candidates
|
||||
.into_iter()
|
||||
.filter_map(|ent| {
|
||||
ent.get_ava_single_url(Attribute::OAuth2RsOrigin)
|
||||
.map(|origin_url| {
|
||||
// Copy the origin url to the landing.
|
||||
let modlist = vec![Modify::Present(
|
||||
Attribute::OAuth2RsOriginLanding,
|
||||
Value::Url(origin_url.clone()),
|
||||
)];
|
||||
|
||||
(ent.get_uuid(), ModifyList::new_list(modlist))
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
// If there is nothing, we don't need to do anything.
|
||||
if !modset.is_empty() {
|
||||
self.internal_batch_modify(modset.into_iter())?;
|
||||
}
|
||||
|
||||
// Do this before schema change since domain info has cookie key
|
||||
// as may at this point.
|
||||
//
|
||||
// Domain info should have the attribute private cookie key removed.
|
||||
let modlist = ModifyList::new_list(vec![
|
||||
Modify::Purged(Attribute::PrivateCookieKey),
|
||||
Modify::Purged(Attribute::Es256PrivateKeyDer),
|
||||
Modify::Purged(Attribute::FernetPrivateKeyStr),
|
||||
]);
|
||||
|
||||
self.internal_modify_uuid(UUID_DOMAIN_INFO, &modlist)?;
|
||||
|
||||
let filter = filter!(f_or!([
|
||||
f_eq(Attribute::Class, EntryClass::ServiceAccount.into()),
|
||||
f_eq(Attribute::Class, EntryClass::SyncAccount.into())
|
||||
]));
|
||||
|
||||
let modlist = ModifyList::new_list(vec![Modify::Purged(Attribute::JwsEs256PrivateKey)]);
|
||||
|
||||
self.internal_modify(&filter, &modlist)?;
|
||||
|
||||
// Now update schema
|
||||
let idm_schema_classes = [
|
||||
SCHEMA_ATTR_PATCH_LEVEL_DL7.clone().into(),
|
||||
SCHEMA_ATTR_DOMAIN_DEVELOPMENT_TAINT_DL7.clone().into(),
|
||||
SCHEMA_ATTR_REFERS_DL7.clone().into(),
|
||||
SCHEMA_ATTR_CERTIFICATE_DL7.clone().into(),
|
||||
SCHEMA_ATTR_OAUTH2_RS_ORIGIN_DL7.clone().into(),
|
||||
SCHEMA_ATTR_OAUTH2_STRICT_REDIRECT_URI_DL7.clone().into(),
|
||||
SCHEMA_ATTR_MAIL_DL7.clone().into(),
|
||||
SCHEMA_ATTR_LEGALNAME_DL7.clone().into(),
|
||||
SCHEMA_ATTR_DISPLAYNAME_DL7.clone().into(),
|
||||
SCHEMA_CLASS_DOMAIN_INFO_DL7.clone().into(),
|
||||
SCHEMA_CLASS_SERVICE_ACCOUNT_DL7.clone().into(),
|
||||
SCHEMA_CLASS_SYNC_ACCOUNT_DL7.clone().into(),
|
||||
SCHEMA_CLASS_CLIENT_CERTIFICATE_DL7.clone().into(),
|
||||
SCHEMA_CLASS_OAUTH2_RS_DL7.clone().into(),
|
||||
];
|
||||
|
||||
idm_schema_classes
|
||||
.into_iter()
|
||||
.try_for_each(|entry| self.internal_migrate_or_create(entry))
|
||||
.map_err(|err| {
|
||||
error!(?err, "migrate_domain_6_to_7 -> Error");
|
||||
err
|
||||
})?;
|
||||
|
||||
self.reload()?;
|
||||
|
||||
// Update access controls
|
||||
let idm_data = [
|
||||
BUILTIN_GROUP_PEOPLE_SELF_NAME_WRITE_DL7
|
||||
.clone()
|
||||
.try_into()?,
|
||||
IDM_PEOPLE_SELF_MAIL_WRITE_DL7.clone().try_into()?,
|
||||
BUILTIN_GROUP_CLIENT_CERTIFICATE_ADMINS_DL7
|
||||
.clone()
|
||||
.try_into()?,
|
||||
IDM_HIGH_PRIVILEGE_DL7.clone().try_into()?,
|
||||
];
|
||||
|
||||
idm_data
|
||||
.into_iter()
|
||||
.try_for_each(|entry| {
|
||||
self.internal_migrate_or_create_ignore_attrs(entry, &[Attribute::Member])
|
||||
})
|
||||
.map_err(|err| {
|
||||
error!(?err, "migrate_domain_6_to_7 -> Error");
|
||||
err
|
||||
})?;
|
||||
|
||||
let idm_data = [
|
||||
IDM_ACP_SELF_WRITE_DL7.clone().into(),
|
||||
IDM_ACP_SELF_NAME_WRITE_DL7.clone().into(),
|
||||
IDM_ACP_HP_CLIENT_CERTIFICATE_MANAGER_DL7.clone().into(),
|
||||
IDM_ACP_OAUTH2_MANAGE_DL7.clone().into(),
|
||||
];
|
||||
|
||||
idm_data
|
||||
.into_iter()
|
||||
.try_for_each(|entry| self.internal_migrate_or_create(entry))
|
||||
.map_err(|err| {
|
||||
error!(?err, "migrate_domain_6_to_7 -> Error");
|
||||
err
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Patch Application - This triggers a one-shot fixup task for issue #2756
|
||||
/// to correct the content of dyngroups after the dyngroups are now loaded.
|
||||
#[instrument(level = "info", skip_all)]
|
||||
pub(crate) fn migrate_domain_patch_level_1(&mut self) -> Result<(), OperationError> {
|
||||
admin_warn!("applying domain patch 1.");
|
||||
|
||||
debug_assert!(*self.phase >= ServerPhase::SchemaReady);
|
||||
|
||||
let filter = filter!(f_eq(Attribute::Class, EntryClass::DynGroup.into()));
|
||||
let modlist = modlist!([m_pres(Attribute::Class, &EntryClass::DynGroup.into())]);
|
||||
|
||||
self.internal_modify(&filter, &modlist).map(|()| {
|
||||
info!("forced dyngroups to re-calculate memberships");
|
||||
})
|
||||
}
|
||||
|
||||
/// Migration domain level 7 to 8
|
||||
#[instrument(level = "info", skip_all)]
|
||||
pub(crate) fn migrate_domain_7_to_8(&mut self) -> Result<(), OperationError> {
|
||||
if !cfg!(test) && DOMAIN_MAX_LEVEL < DOMAIN_LEVEL_8 {
|
||||
error!("Unable to raise domain level from 7 to 8.");
|
||||
return Err(OperationError::MG0004DomainLevelInDevelopment);
|
||||
}
|
||||
|
||||
// ============== Apply constraints ===============
|
||||
let filter = filter!(f_and!([
|
||||
f_eq(Attribute::Class, EntryClass::Account.into()),
|
||||
f_pres(Attribute::PrimaryCredential),
|
||||
]));
|
||||
|
||||
let results = self.internal_search(filter)?;
|
||||
|
||||
let affected_entries = results
|
||||
.into_iter()
|
||||
.filter_map(|entry| {
|
||||
if entry
|
||||
.get_ava_single_credential(Attribute::PrimaryCredential)
|
||||
.map(|cred| cred.has_securitykey())
|
||||
.unwrap_or_default()
|
||||
{
|
||||
Some(entry.get_display_id())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !affected_entries.is_empty() {
|
||||
error!("Unable to proceed. Some accounts still use legacy security keys, which need to be removed.");
|
||||
for sk_present in affected_entries {
|
||||
error!(%sk_present);
|
||||
}
|
||||
return Err(OperationError::MG0006SKConstraintsNotMet);
|
||||
}
|
||||
|
||||
// Check oauth2 strict uri
|
||||
let filter = filter!(f_and!([
|
||||
f_eq(Attribute::Class, EntryClass::OAuth2ResourceServer.into()),
|
||||
f_andnot(f_pres(Attribute::OAuth2StrictRedirectUri)),
|
||||
]));
|
||||
|
||||
let results = self.internal_search(filter)?;
|
||||
|
||||
let affected_entries = results
|
||||
.into_iter()
|
||||
.map(|entry| entry.get_display_id())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !affected_entries.is_empty() {
|
||||
error!("Unable to proceed. Not all oauth2 clients have strict redirect verification enabled.");
|
||||
for missing_oauth2_strict_redirect_uri in affected_entries {
|
||||
error!(%missing_oauth2_strict_redirect_uri);
|
||||
}
|
||||
return Err(OperationError::MG0007Oauth2StrictConstraintsNotMet);
|
||||
}
|
||||
|
||||
// =========== Apply changes ==============
|
||||
|
||||
let idm_schema_classes = [
|
||||
SCHEMA_ATTR_LINKED_GROUP_DL8.clone().into(),
|
||||
SCHEMA_ATTR_APPLICATION_PASSWORD_DL8.clone().into(),
|
||||
SCHEMA_CLASS_APPLICATION_DL8.clone().into(),
|
||||
SCHEMA_CLASS_PERSON_DL8.clone().into(),
|
||||
SCHEMA_CLASS_DOMAIN_INFO_DL8.clone().into(),
|
||||
SCHEMA_ATTR_ALLOW_PRIMARY_CRED_FALLBACK_DL8.clone().into(),
|
||||
SCHEMA_CLASS_ACCOUNT_POLICY_DL8.clone().into(),
|
||||
];
|
||||
|
||||
idm_schema_classes
|
||||
.into_iter()
|
||||
.try_for_each(|entry| self.internal_migrate_or_create(entry))
|
||||
.map_err(|err| {
|
||||
error!(?err, "migrate_domain_6_to_7 -> Error");
|
||||
err
|
||||
})?;
|
||||
|
||||
self.reload()?;
|
||||
|
||||
// Update access controls.
|
||||
let idm_data = [
|
||||
BUILTIN_GROUP_APPLICATION_ADMINS.clone().try_into()?,
|
||||
IDM_ACP_SELF_READ_DL8.clone().into(),
|
||||
IDM_ACP_SELF_WRITE_DL8.clone().into(),
|
||||
IDM_ACP_APPLICATION_MANAGE_DL8.clone().into(),
|
||||
IDM_ACP_APPLICATION_ENTRY_MANAGER_DL8.clone().into(),
|
||||
// Add the new types for mail server
|
||||
BUILTIN_GROUP_MAIL_SERVICE_ADMINS_DL8.clone().try_into()?,
|
||||
BUILTIN_IDM_MAIL_SERVERS_DL8.clone().try_into()?,
|
||||
IDM_ACP_MAIL_SERVERS_DL8.clone().into(),
|
||||
IDM_ACP_DOMAIN_ADMIN_DL8.clone().into(),
|
||||
IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL8.clone().into(),
|
||||
];
|
||||
|
||||
idm_data
|
||||
.into_iter()
|
||||
.try_for_each(|entry| self.internal_migrate_or_create(entry))
|
||||
.map_err(|err| {
|
||||
error!(?err, "migrate_domain_7_to_8 -> Error");
|
||||
err
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Migration domain level 8 to 9 (1.5.0)
|
||||
#[instrument(level = "info", skip_all)]
|
||||
pub(crate) fn migrate_domain_8_to_9(&mut self) -> Result<(), OperationError> {
|
||||
|
@ -760,7 +420,33 @@ impl QueryServerWriteTransaction<'_> {
|
|||
#[instrument(level = "info", skip_all)]
|
||||
pub(crate) fn migrate_domain_9_to_10(&mut self) -> Result<(), OperationError> {
|
||||
if !cfg!(test) && DOMAIN_TGT_LEVEL < DOMAIN_LEVEL_9 {
|
||||
error!("Unable to raise domain level from 8 to 9.");
|
||||
error!("Unable to raise domain level from 9 to 10.");
|
||||
return Err(OperationError::MG0004DomainLevelInDevelopment);
|
||||
}
|
||||
|
||||
// =========== Apply changes ==============
|
||||
|
||||
// Now update schema
|
||||
let idm_schema_changes = [SCHEMA_CLASS_DOMAIN_INFO_DL10.clone().into()];
|
||||
|
||||
idm_schema_changes
|
||||
.into_iter()
|
||||
.try_for_each(|entry| self.internal_migrate_or_create(entry))
|
||||
.map_err(|err| {
|
||||
error!(?err, "migrate_domain_9_to_10 -> Error");
|
||||
err
|
||||
})?;
|
||||
|
||||
self.reload()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Migration domain level 10 to 11 (1.7.0)
|
||||
#[instrument(level = "info", skip_all)]
|
||||
pub(crate) fn migrate_domain_10_to_11(&mut self) -> Result<(), OperationError> {
|
||||
if !cfg!(test) && DOMAIN_TGT_LEVEL < DOMAIN_LEVEL_10 {
|
||||
error!("Unable to raise domain level from 10 to 11.");
|
||||
return Err(OperationError::MG0004DomainLevelInDevelopment);
|
||||
}
|
||||
|
||||
|
@ -817,7 +503,7 @@ impl QueryServerWriteTransaction<'_> {
|
|||
//
|
||||
// DO NOT MODIFY THIS DEFINITION
|
||||
let idm_schema: Vec<EntryInitNew> = vec![
|
||||
SCHEMA_ATTR_MAIL.clone().into(),
|
||||
// SCHEMA_ATTR_MAIL.clone().into(),
|
||||
SCHEMA_ATTR_ACCOUNT_EXPIRE.clone().into(),
|
||||
SCHEMA_ATTR_ACCOUNT_VALID_FROM.clone().into(),
|
||||
SCHEMA_ATTR_API_TOKEN_SESSION.clone().into(),
|
||||
|
@ -827,7 +513,7 @@ impl QueryServerWriteTransaction<'_> {
|
|||
SCHEMA_ATTR_BADLIST_PASSWORD.clone().into(),
|
||||
SCHEMA_ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN.clone().into(),
|
||||
SCHEMA_ATTR_ATTESTED_PASSKEYS.clone().into(),
|
||||
SCHEMA_ATTR_DISPLAYNAME.clone().into(),
|
||||
// SCHEMA_ATTR_DISPLAYNAME.clone().into(),
|
||||
SCHEMA_ATTR_DOMAIN_DISPLAY_NAME.clone().into(),
|
||||
SCHEMA_ATTR_DOMAIN_LDAP_BASEDN.clone().into(),
|
||||
SCHEMA_ATTR_DOMAIN_NAME.clone().into(),
|
||||
|
@ -842,7 +528,7 @@ impl QueryServerWriteTransaction<'_> {
|
|||
SCHEMA_ATTR_GIDNUMBER.clone().into(),
|
||||
SCHEMA_ATTR_GRANT_UI_HINT.clone().into(),
|
||||
SCHEMA_ATTR_JWS_ES256_PRIVATE_KEY.clone().into(),
|
||||
SCHEMA_ATTR_LEGALNAME.clone().into(),
|
||||
// SCHEMA_ATTR_LEGALNAME.clone().into(),
|
||||
SCHEMA_ATTR_LOGINSHELL.clone().into(),
|
||||
SCHEMA_ATTR_NAME_HISTORY.clone().into(),
|
||||
SCHEMA_ATTR_NSUNIQUEID.clone().into(),
|
||||
|
@ -856,7 +542,7 @@ impl QueryServerWriteTransaction<'_> {
|
|||
SCHEMA_ATTR_OAUTH2_RS_IMPLICIT_SCOPES.clone().into(),
|
||||
SCHEMA_ATTR_OAUTH2_RS_NAME.clone().into(),
|
||||
SCHEMA_ATTR_OAUTH2_RS_ORIGIN_LANDING.clone().into(),
|
||||
SCHEMA_ATTR_OAUTH2_RS_ORIGIN.clone().into(),
|
||||
// SCHEMA_ATTR_OAUTH2_RS_ORIGIN.clone().into(),
|
||||
SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP.clone().into(),
|
||||
SCHEMA_ATTR_OAUTH2_RS_SUP_SCOPE_MAP.clone().into(),
|
||||
SCHEMA_ATTR_OAUTH2_RS_TOKEN_KEY.clone().into(),
|
||||
|
@ -891,6 +577,17 @@ impl QueryServerWriteTransaction<'_> {
|
|||
// DL7
|
||||
SCHEMA_ATTR_PATCH_LEVEL_DL7.clone().into(),
|
||||
SCHEMA_ATTR_DOMAIN_DEVELOPMENT_TAINT_DL7.clone().into(),
|
||||
SCHEMA_ATTR_REFERS_DL7.clone().into(),
|
||||
SCHEMA_ATTR_CERTIFICATE_DL7.clone().into(),
|
||||
SCHEMA_ATTR_OAUTH2_RS_ORIGIN_DL7.clone().into(),
|
||||
SCHEMA_ATTR_OAUTH2_STRICT_REDIRECT_URI_DL7.clone().into(),
|
||||
SCHEMA_ATTR_MAIL_DL7.clone().into(),
|
||||
SCHEMA_ATTR_LEGALNAME_DL7.clone().into(),
|
||||
SCHEMA_ATTR_DISPLAYNAME_DL7.clone().into(),
|
||||
// DL8
|
||||
SCHEMA_ATTR_LINKED_GROUP_DL8.clone().into(),
|
||||
SCHEMA_ATTR_APPLICATION_PASSWORD_DL8.clone().into(),
|
||||
SCHEMA_ATTR_ALLOW_PRIMARY_CRED_FALLBACK_DL8.clone().into(),
|
||||
];
|
||||
|
||||
let r = idm_schema
|
||||
|
@ -917,14 +614,14 @@ impl QueryServerWriteTransaction<'_> {
|
|||
// DL4
|
||||
SCHEMA_CLASS_OAUTH2_RS_PUBLIC_DL4.clone().into(),
|
||||
// DL5
|
||||
SCHEMA_CLASS_PERSON_DL5.clone().into(),
|
||||
// SCHEMA_CLASS_PERSON_DL5.clone().into(),
|
||||
SCHEMA_CLASS_ACCOUNT_DL5.clone().into(),
|
||||
SCHEMA_CLASS_OAUTH2_RS_DL5.clone().into(),
|
||||
// SCHEMA_CLASS_OAUTH2_RS_DL5.clone().into(),
|
||||
SCHEMA_CLASS_OAUTH2_RS_BASIC_DL5.clone().into(),
|
||||
// DL6
|
||||
SCHEMA_CLASS_ACCOUNT_POLICY_DL6.clone().into(),
|
||||
SCHEMA_CLASS_SERVICE_ACCOUNT_DL6.clone().into(),
|
||||
SCHEMA_CLASS_SYNC_ACCOUNT_DL6.clone().into(),
|
||||
// SCHEMA_CLASS_ACCOUNT_POLICY_DL6.clone().into(),
|
||||
// SCHEMA_CLASS_SERVICE_ACCOUNT_DL6.clone().into(),
|
||||
// SCHEMA_CLASS_SYNC_ACCOUNT_DL6.clone().into(),
|
||||
SCHEMA_CLASS_GROUP_DL6.clone().into(),
|
||||
SCHEMA_CLASS_KEY_PROVIDER_DL6.clone().into(),
|
||||
SCHEMA_CLASS_KEY_PROVIDER_INTERNAL_DL6.clone().into(),
|
||||
|
@ -932,7 +629,18 @@ impl QueryServerWriteTransaction<'_> {
|
|||
SCHEMA_CLASS_KEY_OBJECT_JWT_ES256_DL6.clone().into(),
|
||||
SCHEMA_CLASS_KEY_OBJECT_JWE_A128GCM_DL6.clone().into(),
|
||||
SCHEMA_CLASS_KEY_OBJECT_INTERNAL_DL6.clone().into(),
|
||||
SCHEMA_CLASS_DOMAIN_INFO_DL6.clone().into(),
|
||||
// SCHEMA_CLASS_DOMAIN_INFO_DL6.clone().into(),
|
||||
// DL7
|
||||
// SCHEMA_CLASS_DOMAIN_INFO_DL7.clone().into(),
|
||||
SCHEMA_CLASS_SERVICE_ACCOUNT_DL7.clone().into(),
|
||||
SCHEMA_CLASS_SYNC_ACCOUNT_DL7.clone().into(),
|
||||
SCHEMA_CLASS_CLIENT_CERTIFICATE_DL7.clone().into(),
|
||||
SCHEMA_CLASS_OAUTH2_RS_DL7.clone().into(),
|
||||
// DL8
|
||||
SCHEMA_CLASS_ACCOUNT_POLICY_DL8.clone().into(),
|
||||
SCHEMA_CLASS_APPLICATION_DL8.clone().into(),
|
||||
SCHEMA_CLASS_PERSON_DL8.clone().into(),
|
||||
SCHEMA_CLASS_DOMAIN_INFO_DL8.clone().into(),
|
||||
];
|
||||
|
||||
let r: Result<(), _> = idm_schema_classes_dl1
|
||||
|
@ -1023,10 +731,10 @@ impl QueryServerWriteTransaction<'_> {
|
|||
IDM_ACP_RADIUS_SERVERS_V1.clone(),
|
||||
IDM_ACP_RADIUS_SECRET_MANAGE_V1.clone(),
|
||||
IDM_ACP_PEOPLE_SELF_WRITE_MAIL_V1.clone(),
|
||||
IDM_ACP_SELF_READ_V1.clone(),
|
||||
IDM_ACP_SELF_WRITE_V1.clone(),
|
||||
// IDM_ACP_SELF_READ_V1.clone(),
|
||||
// IDM_ACP_SELF_WRITE_V1.clone(),
|
||||
IDM_ACP_ACCOUNT_SELF_WRITE_V1.clone(),
|
||||
IDM_ACP_SELF_NAME_WRITE_V1.clone(),
|
||||
// IDM_ACP_SELF_NAME_WRITE_V1.clone(),
|
||||
IDM_ACP_ALL_ACCOUNTS_POSIX_READ_V1.clone(),
|
||||
IDM_ACP_SYSTEM_CONFIG_ACCOUNT_POLICY_MANAGE_V1.clone(),
|
||||
IDM_ACP_GROUP_UNIX_MANAGE_V1.clone(),
|
||||
|
@ -1048,13 +756,26 @@ impl QueryServerWriteTransaction<'_> {
|
|||
IDM_ACP_SERVICE_ACCOUNT_MANAGE_V1.clone(),
|
||||
// DL4
|
||||
// DL5
|
||||
IDM_ACP_OAUTH2_MANAGE_DL5.clone(),
|
||||
// IDM_ACP_OAUTH2_MANAGE_DL5.clone(),
|
||||
// DL6
|
||||
IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL6.clone(),
|
||||
// IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL6.clone(),
|
||||
IDM_ACP_PEOPLE_CREATE_DL6.clone(),
|
||||
IDM_ACP_GROUP_MANAGE_DL6.clone(),
|
||||
IDM_ACP_ACCOUNT_MAIL_READ_DL6.clone(),
|
||||
IDM_ACP_DOMAIN_ADMIN_DL6.clone(),
|
||||
// IDM_ACP_DOMAIN_ADMIN_DL6.clone(),
|
||||
// DL7
|
||||
// IDM_ACP_SELF_WRITE_DL7.clone(),
|
||||
IDM_ACP_SELF_NAME_WRITE_DL7.clone(),
|
||||
IDM_ACP_HP_CLIENT_CERTIFICATE_MANAGER_DL7.clone(),
|
||||
IDM_ACP_OAUTH2_MANAGE_DL7.clone(),
|
||||
// DL8
|
||||
IDM_ACP_SELF_READ_DL8.clone(),
|
||||
IDM_ACP_SELF_WRITE_DL8.clone(),
|
||||
IDM_ACP_APPLICATION_MANAGE_DL8.clone(),
|
||||
IDM_ACP_APPLICATION_ENTRY_MANAGER_DL8.clone(),
|
||||
IDM_ACP_MAIL_SERVERS_DL8.clone(),
|
||||
IDM_ACP_DOMAIN_ADMIN_DL8.clone(),
|
||||
IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL8.clone(),
|
||||
];
|
||||
|
||||
let res: Result<(), _> = idm_entries
|
||||
|
@ -1084,19 +805,6 @@ impl QueryServerReadTransaction<'_> {
|
|||
|
||||
let mut report_items = Vec::with_capacity(1);
|
||||
|
||||
if current_level <= DOMAIN_LEVEL_6 && upgrade_level >= DOMAIN_LEVEL_7 {
|
||||
let item = self
|
||||
.domain_upgrade_check_6_to_7_gidnumber()
|
||||
.map_err(|err| {
|
||||
error!(
|
||||
?err,
|
||||
"Failed to perform domain upgrade check 6 to 7 - gidnumber"
|
||||
);
|
||||
err
|
||||
})?;
|
||||
report_items.push(item);
|
||||
}
|
||||
|
||||
if current_level <= DOMAIN_LEVEL_7 && upgrade_level >= DOMAIN_LEVEL_8 {
|
||||
let item = self
|
||||
.domain_upgrade_check_7_to_8_security_keys()
|
||||
|
@ -1130,94 +838,6 @@ impl QueryServerReadTransaction<'_> {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn domain_upgrade_check_6_to_7_gidnumber(
|
||||
&mut self,
|
||||
) -> Result<ProtoDomainUpgradeCheckItem, OperationError> {
|
||||
let filter = filter!(f_and!([
|
||||
f_or!([
|
||||
f_eq(Attribute::Class, EntryClass::PosixAccount.into()),
|
||||
f_eq(Attribute::Class, EntryClass::PosixGroup.into())
|
||||
]),
|
||||
// This logic gets a bit messy but it would be:
|
||||
// If ! (
|
||||
// (GID_REGULAR_USER_MIN < value < GID_REGULAR_USER_MAX) ||
|
||||
// (GID_UNUSED_A_MIN < value < GID_UNUSED_A_MAX) ||
|
||||
// (GID_UNUSED_B_MIN < value < GID_UNUSED_B_MAX) ||
|
||||
// (GID_UNUSED_C_MIN < value < GID_UNUSED_D_MAX)
|
||||
// )
|
||||
f_andnot(f_or!([
|
||||
f_and!([
|
||||
// The gid value must be less than GID_REGULAR_USER_MAX
|
||||
f_lt(
|
||||
Attribute::GidNumber,
|
||||
PartialValue::Uint32(crate::plugins::gidnumber::GID_REGULAR_USER_MAX)
|
||||
),
|
||||
// This bit of mental gymnastics is "greater than".
|
||||
// The gid value must not be less than USER_MIN
|
||||
f_andnot(f_lt(
|
||||
Attribute::GidNumber,
|
||||
PartialValue::Uint32(crate::plugins::gidnumber::GID_REGULAR_USER_MIN)
|
||||
))
|
||||
]),
|
||||
f_and!([
|
||||
f_lt(
|
||||
Attribute::GidNumber,
|
||||
PartialValue::Uint32(crate::plugins::gidnumber::GID_UNUSED_A_MAX)
|
||||
),
|
||||
f_andnot(f_lt(
|
||||
Attribute::GidNumber,
|
||||
PartialValue::Uint32(crate::plugins::gidnumber::GID_UNUSED_A_MIN)
|
||||
))
|
||||
]),
|
||||
f_and!([
|
||||
f_lt(
|
||||
Attribute::GidNumber,
|
||||
PartialValue::Uint32(crate::plugins::gidnumber::GID_UNUSED_B_MAX)
|
||||
),
|
||||
f_andnot(f_lt(
|
||||
Attribute::GidNumber,
|
||||
PartialValue::Uint32(crate::plugins::gidnumber::GID_UNUSED_B_MIN)
|
||||
))
|
||||
]),
|
||||
// If both of these conditions are true we get:
|
||||
// C_MIN < value < D_MAX, which the outer and-not inverts.
|
||||
f_and!([
|
||||
// The gid value must be less than GID_UNUSED_D_MAX
|
||||
f_lt(
|
||||
Attribute::GidNumber,
|
||||
PartialValue::Uint32(crate::plugins::gidnumber::GID_UNUSED_D_MAX)
|
||||
),
|
||||
// This bit of mental gymnastics is "greater than".
|
||||
// The gid value must not be less than C_MIN
|
||||
f_andnot(f_lt(
|
||||
Attribute::GidNumber,
|
||||
PartialValue::Uint32(crate::plugins::gidnumber::GID_UNUSED_C_MIN)
|
||||
))
|
||||
]),
|
||||
]))
|
||||
]));
|
||||
|
||||
let results = self.internal_search(filter)?;
|
||||
|
||||
let affected_entries = results
|
||||
.into_iter()
|
||||
.map(|entry| entry.get_display_id())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let status = if affected_entries.is_empty() {
|
||||
ProtoDomainUpgradeCheckStatus::Pass6To7Gidnumber
|
||||
} else {
|
||||
ProtoDomainUpgradeCheckStatus::Fail6To7Gidnumber
|
||||
};
|
||||
|
||||
Ok(ProtoDomainUpgradeCheckItem {
|
||||
status,
|
||||
from_level: DOMAIN_LEVEL_6,
|
||||
to_level: DOMAIN_LEVEL_7,
|
||||
affected_entries,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn domain_upgrade_check_7_to_8_security_keys(
|
||||
&mut self,
|
||||
) -> Result<ProtoDomainUpgradeCheckItem, OperationError> {
|
||||
|
@ -1289,7 +909,7 @@ impl QueryServerReadTransaction<'_> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ProtoDomainUpgradeCheckItem, ProtoDomainUpgradeCheckStatus};
|
||||
// use super::{ProtoDomainUpgradeCheckItem, ProtoDomainUpgradeCheckStatus};
|
||||
use crate::prelude::*;
|
||||
|
||||
#[qs_test]
|
||||
|
@ -1318,9 +938,8 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[qs_test(domain_level=DOMAIN_LEVEL_6)]
|
||||
async fn test_migrations_dl6_dl7(server: &QueryServer) {
|
||||
// Assert our instance was setup to version 6
|
||||
#[qs_test(domain_level=DOMAIN_LEVEL_8)]
|
||||
async fn test_migrations_dl8_dl9(server: &QueryServer) {
|
||||
let mut write_txn = server.write(duration_from_epoch_now()).await.unwrap();
|
||||
|
||||
let db_domain_version = write_txn
|
||||
|
@ -1329,164 +948,95 @@ mod tests {
|
|||
.get_ava_single_uint32(Attribute::Version)
|
||||
.expect("Attribute Version not present");
|
||||
|
||||
assert_eq!(db_domain_version, DOMAIN_LEVEL_6);
|
||||
|
||||
// Create an oauth2 client that doesn't have a landing url set.
|
||||
let oauth2_client_uuid = Uuid::new_v4();
|
||||
|
||||
let ea: Entry<EntryInit, EntryNew> = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Uuid, Value::Uuid(oauth2_client_uuid)),
|
||||
(
|
||||
Attribute::Class,
|
||||
EntryClass::OAuth2ResourceServer.to_value()
|
||||
),
|
||||
(
|
||||
Attribute::Class,
|
||||
EntryClass::OAuth2ResourceServerPublic.to_value()
|
||||
),
|
||||
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||
(
|
||||
Attribute::DisplayName,
|
||||
Value::new_utf8s("test_resource_server")
|
||||
),
|
||||
(
|
||||
Attribute::OAuth2RsOrigin,
|
||||
Value::new_url_s("https://demo.example.com").unwrap()
|
||||
)
|
||||
);
|
||||
|
||||
write_txn
|
||||
.internal_create(vec![ea])
|
||||
.expect("Unable to create oauth2 client");
|
||||
|
||||
// Set the version to 7.
|
||||
write_txn
|
||||
.internal_apply_domain_migration(DOMAIN_LEVEL_7)
|
||||
.expect("Unable to set domain level to version 7");
|
||||
|
||||
// post migration verification.
|
||||
let domain_entry = write_txn
|
||||
.internal_search_uuid(UUID_DOMAIN_INFO)
|
||||
.expect("Unable to access domain entry");
|
||||
|
||||
assert!(!domain_entry.attribute_pres(Attribute::PrivateCookieKey));
|
||||
|
||||
let oauth2_entry = write_txn
|
||||
.internal_search_uuid(oauth2_client_uuid)
|
||||
.expect("Unable to access oauth2 client entry");
|
||||
|
||||
let origin = oauth2_entry
|
||||
.get_ava_single_url(Attribute::OAuth2RsOrigin)
|
||||
.expect("Unable to access oauth2 client origin");
|
||||
|
||||
// The origin should have been cloned to the landing.
|
||||
let landing = oauth2_entry
|
||||
.get_ava_single_url(Attribute::OAuth2RsOriginLanding)
|
||||
.expect("Unable to access oauth2 client landing");
|
||||
|
||||
assert_eq!(origin, landing);
|
||||
|
||||
write_txn.commit().expect("Unable to commit");
|
||||
}
|
||||
|
||||
#[qs_test(domain_level=DOMAIN_LEVEL_7)]
|
||||
async fn test_migrations_dl7_dl8(server: &QueryServer) {
|
||||
// Assert our instance was setup to version 7
|
||||
let mut write_txn = server.write(duration_from_epoch_now()).await.unwrap();
|
||||
|
||||
let db_domain_version = write_txn
|
||||
.internal_search_uuid(UUID_DOMAIN_INFO)
|
||||
.expect("unable to access domain entry")
|
||||
.get_ava_single_uint32(Attribute::Version)
|
||||
.expect("Attribute Version not present");
|
||||
|
||||
assert_eq!(db_domain_version, DOMAIN_LEVEL_7);
|
||||
|
||||
// Create an oauth2 client that doesn't have a landing url set.
|
||||
let oauth2_client_uuid = Uuid::new_v4();
|
||||
|
||||
let ea: Entry<EntryInit, EntryNew> = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Uuid, Value::Uuid(oauth2_client_uuid)),
|
||||
(
|
||||
Attribute::Class,
|
||||
EntryClass::OAuth2ResourceServer.to_value()
|
||||
),
|
||||
(
|
||||
Attribute::Class,
|
||||
EntryClass::OAuth2ResourceServerPublic.to_value()
|
||||
),
|
||||
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||
(
|
||||
Attribute::DisplayName,
|
||||
Value::new_utf8s("test_resource_server")
|
||||
),
|
||||
(
|
||||
Attribute::OAuth2RsOriginLanding,
|
||||
Value::new_url_s("https://demo.example.com/oauth2").unwrap()
|
||||
),
|
||||
(
|
||||
Attribute::OAuth2RsOrigin,
|
||||
Value::new_url_s("https://demo.example.com").unwrap()
|
||||
)
|
||||
);
|
||||
|
||||
write_txn
|
||||
.internal_create(vec![ea])
|
||||
.expect("Unable to create oauth2 client");
|
||||
assert_eq!(db_domain_version, DOMAIN_LEVEL_8);
|
||||
|
||||
write_txn.commit().expect("Unable to commit");
|
||||
|
||||
// pre migration verification.
|
||||
// == pre migration verification. ==
|
||||
// check we currently would fail a migration.
|
||||
|
||||
let mut read_txn = server.read().await.unwrap();
|
||||
|
||||
match read_txn.domain_upgrade_check_7_to_8_oauth2_strict_redirect_uri() {
|
||||
Ok(ProtoDomainUpgradeCheckItem {
|
||||
status: ProtoDomainUpgradeCheckStatus::Fail7To8Oauth2StrictRedirectUri,
|
||||
..
|
||||
}) => {
|
||||
trace!("Failed as expected, very good.");
|
||||
}
|
||||
other => {
|
||||
error!(?other);
|
||||
unreachable!();
|
||||
}
|
||||
};
|
||||
|
||||
drop(read_txn);
|
||||
|
||||
// Okay, fix the problem.
|
||||
// let mut read_txn = server.read().await.unwrap();
|
||||
// drop(read_txn);
|
||||
|
||||
let mut write_txn = server.write(duration_from_epoch_now()).await.unwrap();
|
||||
|
||||
write_txn
|
||||
.internal_modify_uuid(
|
||||
oauth2_client_uuid,
|
||||
&ModifyList::new_purge_and_set(
|
||||
Attribute::OAuth2StrictRedirectUri,
|
||||
Value::Bool(true),
|
||||
),
|
||||
)
|
||||
.expect("Unable to enforce strict mode.");
|
||||
// Fix any issues
|
||||
|
||||
// Set the version to 8.
|
||||
// == Increase the version ==
|
||||
write_txn
|
||||
.internal_apply_domain_migration(DOMAIN_LEVEL_8)
|
||||
.expect("Unable to set domain level to version 8");
|
||||
.internal_apply_domain_migration(DOMAIN_LEVEL_9)
|
||||
.expect("Unable to set domain level to version 9");
|
||||
|
||||
// post migration verification.
|
||||
|
||||
write_txn.commit().expect("Unable to commit");
|
||||
}
|
||||
|
||||
#[qs_test(domain_level=DOMAIN_LEVEL_8)]
|
||||
async fn test_migrations_dl8_dl9(_server: &QueryServer) {}
|
||||
|
||||
#[qs_test(domain_level=DOMAIN_LEVEL_9)]
|
||||
async fn test_migrations_dl9_dl10(_server: &QueryServer) {}
|
||||
async fn test_migrations_dl9_dl10(server: &QueryServer) {
|
||||
let mut write_txn = server.write(duration_from_epoch_now()).await.unwrap();
|
||||
|
||||
let db_domain_version = write_txn
|
||||
.internal_search_uuid(UUID_DOMAIN_INFO)
|
||||
.expect("unable to access domain entry")
|
||||
.get_ava_single_uint32(Attribute::Version)
|
||||
.expect("Attribute Version not present");
|
||||
|
||||
assert_eq!(db_domain_version, DOMAIN_LEVEL_9);
|
||||
|
||||
write_txn.commit().expect("Unable to commit");
|
||||
|
||||
// == pre migration verification. ==
|
||||
// check we currently would fail a migration.
|
||||
|
||||
// let mut read_txn = server.read().await.unwrap();
|
||||
// drop(read_txn);
|
||||
|
||||
let mut write_txn = server.write(duration_from_epoch_now()).await.unwrap();
|
||||
|
||||
// Fix any issues
|
||||
|
||||
// == Increase the version ==
|
||||
write_txn
|
||||
.internal_apply_domain_migration(DOMAIN_LEVEL_10)
|
||||
.expect("Unable to set domain level to version 10");
|
||||
|
||||
// post migration verification.
|
||||
|
||||
write_txn.commit().expect("Unable to commit");
|
||||
}
|
||||
|
||||
#[qs_test(domain_level=DOMAIN_LEVEL_10)]
|
||||
async fn test_migrations_dl10_dl11(server: &QueryServer) {
|
||||
let mut write_txn = server.write(duration_from_epoch_now()).await.unwrap();
|
||||
|
||||
let db_domain_version = write_txn
|
||||
.internal_search_uuid(UUID_DOMAIN_INFO)
|
||||
.expect("unable to access domain entry")
|
||||
.get_ava_single_uint32(Attribute::Version)
|
||||
.expect("Attribute Version not present");
|
||||
|
||||
assert_eq!(db_domain_version, DOMAIN_LEVEL_10);
|
||||
|
||||
write_txn.commit().expect("Unable to commit");
|
||||
|
||||
// == pre migration verification. ==
|
||||
// check we currently would fail a migration.
|
||||
|
||||
// let mut read_txn = server.read().await.unwrap();
|
||||
// drop(read_txn);
|
||||
|
||||
let mut write_txn = server.write(duration_from_epoch_now()).await.unwrap();
|
||||
|
||||
// Fix any issues
|
||||
|
||||
// == Increase the version ==
|
||||
write_txn
|
||||
.internal_apply_domain_migration(DOMAIN_LEVEL_11)
|
||||
.expect("Unable to set domain level to version 11");
|
||||
|
||||
// post migration verification.
|
||||
|
||||
write_txn.commit().expect("Unable to commit");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1242,13 +1242,6 @@ pub trait QueryServerTransaction<'a> {
|
|||
}
|
||||
|
||||
fn get_domain_key_object_handle(&self) -> Result<Arc<KeyObject>, OperationError> {
|
||||
#[cfg(test)]
|
||||
if self.get_domain_version() < DOMAIN_LEVEL_6 {
|
||||
// We must be in tests, and this is a DL5 to 6 test. For this we'll just make
|
||||
// an ephemeral provider.
|
||||
return Ok(crate::server::keys::KeyObjectInternal::new_test());
|
||||
};
|
||||
|
||||
self.get_key_providers()
|
||||
.get_key_object_handle(UUID_DOMAIN_INFO)
|
||||
.ok_or(OperationError::KP0031KeyObjectNotFound)
|
||||
|
@ -2339,7 +2332,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
debug!(domain_previous_patch_level = ?previous_patch_level, domain_target_patch_level = ?domain_info_patch_level);
|
||||
|
||||
// We have to check for DL0 since that's the initialisation level.
|
||||
if previous_version <= DOMAIN_LEVEL_5 && previous_version != DOMAIN_LEVEL_0 {
|
||||
if previous_version < DOMAIN_MIN_REMIGRATION_LEVEL && previous_version != DOMAIN_LEVEL_0 {
|
||||
error!("UNABLE TO PROCEED. You are attempting a Skip update which is NOT SUPPORTED. You must upgrade one-version of Kanidm at a time.");
|
||||
error!("For more see: https://kanidm.github.io/kanidm/stable/support.html#upgrade-policy and https://kanidm.github.io/kanidm/stable/server_updates.html");
|
||||
error!(domain_previous_version = ?previous_version, domain_target_version = ?domain_info_version);
|
||||
|
@ -2347,21 +2340,8 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
return Err(OperationError::MG0008SkipUpgradeAttempted);
|
||||
}
|
||||
|
||||
if previous_version <= DOMAIN_LEVEL_6 && domain_info_version >= DOMAIN_LEVEL_7 {
|
||||
self.migrate_domain_6_to_7()?;
|
||||
}
|
||||
|
||||
// Similar to the older system info migration handler, these allow "one shot" fixes
|
||||
// to be issued and run by bumping the patch level.
|
||||
if previous_patch_level < PATCH_LEVEL_1 && domain_info_patch_level >= PATCH_LEVEL_1 {
|
||||
self.migrate_domain_patch_level_1()?;
|
||||
}
|
||||
|
||||
if previous_version <= DOMAIN_LEVEL_7 && domain_info_version >= DOMAIN_LEVEL_8 {
|
||||
self.migrate_domain_7_to_8()?;
|
||||
}
|
||||
|
||||
if previous_version <= DOMAIN_LEVEL_8 && domain_info_version >= DOMAIN_LEVEL_9 {
|
||||
// 1.4 -> 1.5
|
||||
self.migrate_domain_8_to_9()?;
|
||||
}
|
||||
|
||||
|
@ -2370,9 +2350,15 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
}
|
||||
|
||||
if previous_version <= DOMAIN_LEVEL_9 && domain_info_version >= DOMAIN_LEVEL_10 {
|
||||
// 1.5 -> 1.6
|
||||
self.migrate_domain_9_to_10()?;
|
||||
}
|
||||
|
||||
if previous_version <= DOMAIN_LEVEL_10 && domain_info_version >= DOMAIN_LEVEL_11 {
|
||||
// 1.6 -> 1.7
|
||||
self.migrate_domain_10_to_11()?;
|
||||
}
|
||||
|
||||
// This is here to catch when we increase domain levels but didn't create the migration
|
||||
// hooks. If this fails it probably means you need to add another migration hook
|
||||
// in the above.
|
||||
|
@ -2394,7 +2380,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
let display_name = domain_entry
|
||||
.get_ava_single_utf8(Attribute::DomainDisplayName)
|
||||
.map(str::to_string)
|
||||
.ok_or(OperationError::InvalidEntryState)?;
|
||||
.unwrap_or_else(|| format!("Kanidm {}", domain_name));
|
||||
|
||||
let domain_ldap_allow_unix_pw_bind = domain_entry
|
||||
.get_ava_single_bool(Attribute::LdapAllowUnixPwBind)
|
||||
|
|
|
@ -279,6 +279,7 @@ impl ValueSetT for ValueSetOauthScopeMap {
|
|||
match value {
|
||||
Value::OauthScopeMap(u, m) => {
|
||||
match self.map.entry(u) {
|
||||
// We are going to assume that a vacant entry will not be set to empty.
|
||||
BTreeEntry::Vacant(e) => {
|
||||
e.insert(m);
|
||||
Ok(true)
|
||||
|
@ -289,7 +290,12 @@ impl ValueSetT for ValueSetOauthScopeMap {
|
|||
// associated map state. So by always replacing on a present, we are true to
|
||||
// the intent of the api.
|
||||
BTreeEntry::Occupied(mut e) => {
|
||||
e.insert(m);
|
||||
if m.is_empty() {
|
||||
e.remove();
|
||||
} else {
|
||||
e.insert(m);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,6 @@ url = { workspace = true, features = ["serde"] }
|
|||
kanidm_build_profiles = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0.16"
|
||||
compact_jwt = { workspace = true }
|
||||
escargot = "0.5.13"
|
||||
# used for webdriver testing
|
||||
|
@ -59,11 +58,14 @@ oauth2_ext = { workspace = true, default-features = false, features = [
|
|||
"reqwest",
|
||||
] }
|
||||
openssl = { workspace = true }
|
||||
petgraph = { version = "0.7.1", features = ["serde", "serde-1"] }
|
||||
petgraph = { version = "0.7.1", features = ["serde"] }
|
||||
serde_json = { workspace = true }
|
||||
time = { workspace = true }
|
||||
tokio-openssl = { workspace = true }
|
||||
kanidm_lib_crypto = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
webauthn-authenticator-rs = { workspace = true }
|
||||
jsonschema = "0.28.3"
|
||||
jsonschema = "0.29.0"
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["escargot", "futures", "kanidm_build_profiles"]
|
||||
|
|
|
@ -3,14 +3,13 @@
|
|||
//! - @yaleman
|
||||
//!
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
// use kanidm_client::KanidmClient;
|
||||
use kanidmd_lib::constants::entries::Attribute;
|
||||
use kanidmd_lib::constants::groups::{idm_builtin_admin_groups, idm_builtin_non_admin_groups};
|
||||
use kanidmd_lib::prelude::{builtin_accounts, EntryInitNew};
|
||||
use petgraph::graphmap::{AllEdges, GraphMap, NodeTrait};
|
||||
use petgraph::Directed;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
|
|
|
@ -58,7 +58,7 @@ tokio = { workspace = true, features = ["rt", "macros", "fs", "signal"] }
|
|||
url = { workspace = true, features = ["serde"] }
|
||||
uuid = { workspace = true }
|
||||
zxcvbn = { workspace = true }
|
||||
lazy_static.workspace = true
|
||||
lazy_static = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -119,3 +119,6 @@ assets = [
|
|||
],
|
||||
]
|
||||
maintainer-scripts = "debian/"
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["clap_complete", "kanidm_build_profiles"]
|
||||
|
|
|
@ -162,7 +162,7 @@ impl PersonOpt {
|
|||
}
|
||||
PersonPosix::SetPassword(aopt) => {
|
||||
let client = aopt.copt.to_client(OpType::Write).await;
|
||||
let password = match password_prompt("Enter new posix (sudo) password: ") {
|
||||
let password = match password_prompt("Enter new posix (sudo) password") {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
println!("Passwords do not match");
|
||||
|
|
|
@ -254,7 +254,7 @@ async fn do_password(
|
|||
password.to_owned()
|
||||
}
|
||||
None => dialoguer::Password::new()
|
||||
.with_prompt("Enter password: ")
|
||||
.with_prompt("Enter password")
|
||||
.interact()
|
||||
.unwrap_or_else(|e| {
|
||||
error!("Failed to create password prompt -- {:?}", e);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "kanidm_device_flow"
|
||||
description = "Kanidm Device Flow Client"
|
||||
description = "Kanidm Device Flow Example Client"
|
||||
documentation = "https://kanidm.github.io/kanidm/stable/"
|
||||
version = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
|
@ -18,15 +18,12 @@ doctest = false
|
|||
[features]
|
||||
|
||||
[dependencies]
|
||||
kanidm_proto = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
kanidm_proto = { workspace = true }
|
||||
oauth2 = "5.0.0"
|
||||
reqwest = { version = "0.12.12", default-features = false, features = [
|
||||
reqwest = { workspace = true, default-features = false, features = [
|
||||
"rustls-tls",
|
||||
] }
|
||||
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
url = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
sketching = { workspace = true }
|
||||
base64.workspace = true
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tracing = { workspace = true }
|
||||
|
|
|
@ -39,3 +39,6 @@ kanidm_utils_users = { workspace = true }
|
|||
[build-dependencies]
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
clap_complete = { workspace = true }
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["clap_complete"]
|
||||
|
|
|
@ -42,3 +42,6 @@ clap_complete = { workspace = true }
|
|||
|
||||
[dev-dependencies]
|
||||
sketching = { workspace = true }
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["clap_complete"]
|
||||
|
|
|
@ -42,3 +42,5 @@ mimalloc = { workspace = true }
|
|||
[build-dependencies]
|
||||
kanidm_build_profiles = { workspace = true }
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["kanidm_build_profiles"]
|
||||
|
|
|
@ -37,3 +37,6 @@ tracing = { workspace = true }
|
|||
|
||||
[build-dependencies]
|
||||
kanidm_build_profiles = { workspace = true }
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["kanidm_build_profiles"]
|
||||
|
|
|
@ -29,11 +29,11 @@ impl Decoder for ClientCodec {
|
|||
}
|
||||
}
|
||||
|
||||
impl Encoder<ClientRequest> for ClientCodec {
|
||||
impl Encoder<&ClientRequest> for ClientCodec {
|
||||
type Error = IoError;
|
||||
|
||||
fn encode(&mut self, msg: ClientRequest, dst: &mut BytesMut) -> Result<(), Self::Error> {
|
||||
let data = serde_json::to_vec(&msg).map_err(|e| {
|
||||
fn encode(&mut self, msg: &ClientRequest, dst: &mut BytesMut) -> Result<(), Self::Error> {
|
||||
let data = serde_json::to_vec(msg).map_err(|e| {
|
||||
error!("socket encoding error -> {:?}", e);
|
||||
IoError::new(ErrorKind::Other, "JSON encode error")
|
||||
})?;
|
||||
|
@ -49,48 +49,63 @@ impl ClientCodec {
|
|||
}
|
||||
}
|
||||
|
||||
async fn call_daemon_inner(
|
||||
path: &str,
|
||||
req: ClientRequest,
|
||||
) -> Result<ClientResponse, Box<dyn Error>> {
|
||||
trace!(?path, ?req);
|
||||
let stream = UnixStream::connect(path).await?;
|
||||
trace!("connected");
|
||||
pub struct DaemonClient {
|
||||
req_stream: Framed<UnixStream, ClientCodec>,
|
||||
default_timeout: u64,
|
||||
}
|
||||
|
||||
let mut reqs = Framed::new(stream, ClientCodec::new());
|
||||
impl DaemonClient {
|
||||
pub async fn new(path: &str, default_timeout: u64) -> Result<Self, Box<dyn Error>> {
|
||||
trace!(?path);
|
||||
let stream = UnixStream::connect(path).await.inspect_err(|e| {
|
||||
error!(
|
||||
"Unix socket stream setup error while connecting to {} -> {:?}",
|
||||
path, e
|
||||
);
|
||||
})?;
|
||||
|
||||
reqs.send(req).await?;
|
||||
reqs.flush().await?;
|
||||
trace!("flushed, waiting ...");
|
||||
let req_stream = Framed::new(stream, ClientCodec::new());
|
||||
|
||||
match reqs.next().await {
|
||||
Some(Ok(res)) => {
|
||||
debug!("Response -> {:?}", res);
|
||||
Ok(res)
|
||||
trace!("connected");
|
||||
|
||||
Ok(DaemonClient {
|
||||
req_stream,
|
||||
default_timeout,
|
||||
})
|
||||
}
|
||||
|
||||
async fn call_inner(&mut self, req: &ClientRequest) -> Result<ClientResponse, Box<dyn Error>> {
|
||||
self.req_stream.send(req).await?;
|
||||
self.req_stream.flush().await?;
|
||||
trace!("flushed, waiting ...");
|
||||
match self.req_stream.next().await {
|
||||
Some(Ok(res)) => {
|
||||
debug!("Response -> {:?}", res);
|
||||
Ok(res)
|
||||
}
|
||||
_ => {
|
||||
error!("Error making request to kanidm_unixd");
|
||||
Err(Box::new(IoError::new(ErrorKind::Other, "oh no!")))
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
error!("Error making request to kanidm_unixd");
|
||||
Err(Box::new(IoError::new(ErrorKind::Other, "oh no!")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes a call to kanidm_unixd via a unix socket at `path`
|
||||
pub async fn call_daemon(
|
||||
path: &str,
|
||||
req: ClientRequest,
|
||||
timeout: u64,
|
||||
) -> Result<ClientResponse, Box<dyn Error>> {
|
||||
let sleep = time::sleep(Duration::from_secs(timeout));
|
||||
tokio::pin!(sleep);
|
||||
|
||||
tokio::select! {
|
||||
_ = &mut sleep => {
|
||||
error!(?timeout, "Timed out making request to kanidm_unixd");
|
||||
Err(Box::new(IoError::new(ErrorKind::Other, "timeout")))
|
||||
}
|
||||
res = call_daemon_inner(path, req) => {
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn call(
|
||||
&mut self,
|
||||
req: &ClientRequest,
|
||||
timeout: Option<u64>,
|
||||
) -> Result<ClientResponse, Box<dyn Error>> {
|
||||
let sleep = time::sleep(Duration::from_secs(timeout.unwrap_or(self.default_timeout)));
|
||||
tokio::pin!(sleep);
|
||||
|
||||
tokio::select! {
|
||||
_ = &mut sleep => {
|
||||
error!(?timeout, "Timed out making request to kanidm_unixd");
|
||||
Err(Box::new(IoError::new(ErrorKind::Other, "timeout")))
|
||||
}
|
||||
res = self.call_inner(req) => {
|
||||
res
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,3 +17,9 @@ pub const DEFAULT_GID_ATTR_MAP: UidAttr = UidAttr::Spn;
|
|||
pub const DEFAULT_SELINUX: bool = true;
|
||||
pub const DEFAULT_TPM_TCTI_NAME: &str = "device:/dev/tpmrm0";
|
||||
pub const DEFAULT_HSM_PIN_PATH: &str = "/var/lib/kanidm-unixd/hsm-pin";
|
||||
|
||||
#[cfg(all(target_family = "unix", not(target_os = "freebsd")))]
|
||||
pub const DEFAULT_SHELL_SEARCH_PATHS: &[&str] = &["/bin"];
|
||||
|
||||
#[cfg(all(target_family = "unix", target_os = "freebsd"))]
|
||||
pub const DEFAULT_SHELL_SEARCH_PATHS: &[&str] = &["/bin", "/usr/local/bin"];
|
||||
|
|
|
@ -12,8 +12,8 @@ repository = { workspace = true }
|
|||
|
||||
[lib]
|
||||
name = "nss_kanidm"
|
||||
crate-type = [ "cdylib" ]
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["cdylib"]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
kanidm_unix_common = { workspace = true }
|
||||
|
@ -21,7 +21,6 @@ kanidm_unix_common = { workspace = true }
|
|||
[target.'cfg(not(target_family = "windows"))'.dependencies]
|
||||
libnss = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
paste = { workspace = true }
|
||||
lazy_static = { workspace = true }
|
||||
|
||||
[target."cfg(target_os = \"freebsd\")".build-dependencies]
|
||||
|
@ -37,15 +36,27 @@ depends = ""
|
|||
section = "network"
|
||||
priority = "optional"
|
||||
assets = [
|
||||
# Empty on purpose
|
||||
# Empty on purpose
|
||||
]
|
||||
|
||||
[package.metadata.deb.variants.aarch64-unknown-linux-gnu]
|
||||
merge-assets.append = [
|
||||
[ "target/release/libnss_kanidm.so", "usr/lib/aarch64-linux-gnu/libnss_kanidm.so.2", "644"],
|
||||
[
|
||||
"target/release/libnss_kanidm.so",
|
||||
"usr/lib/aarch64-linux-gnu/libnss_kanidm.so.2",
|
||||
"644",
|
||||
],
|
||||
]
|
||||
|
||||
[package.metadata.deb.variants.x86_64-unknown-linux-gnu]
|
||||
merge-assets.append = [
|
||||
[ "target/release/libnss_kanidm.so", "usr/lib/x86_64-linux-gnu/libnss_kanidm.so.2", "644"],
|
||||
[
|
||||
"target/release/libnss_kanidm.so",
|
||||
"usr/lib/x86_64-linux-gnu/libnss_kanidm.so.2",
|
||||
"644",
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["cc", "lazy_static"]
|
||||
|
|
|
@ -13,8 +13,8 @@ repository = { workspace = true }
|
|||
|
||||
[lib]
|
||||
name = "pam_kanidm"
|
||||
crate-type = [ "cdylib" ]
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["cdylib"]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
kanidm_unix_common = { workspace = true }
|
||||
|
@ -37,17 +37,36 @@ section = "network"
|
|||
priority = "optional"
|
||||
maintainer-scripts = "debian/"
|
||||
assets = [
|
||||
# Empty on purpose
|
||||
# Empty on purpose
|
||||
]
|
||||
|
||||
[package.metadata.deb.variants.aarch64-unknown-linux-gnu]
|
||||
merge-assets.append = [
|
||||
[ "target/release/libpam_kanidm.so", "usr/lib/aarch64-linux-gnu/security/pam_kanidm.so", "644"],
|
||||
[ "debian/kanidm.pam", "usr/share/pam-configs/kanidm", "644"],
|
||||
[
|
||||
"target/release/libpam_kanidm.so",
|
||||
"usr/lib/aarch64-linux-gnu/security/pam_kanidm.so",
|
||||
"644",
|
||||
],
|
||||
[
|
||||
"debian/kanidm.pam",
|
||||
"usr/share/pam-configs/kanidm",
|
||||
"644",
|
||||
],
|
||||
]
|
||||
|
||||
[package.metadata.deb.variants.x86_64-unknown-linux-gnu]
|
||||
merge-assets.append = [
|
||||
[ "target/release/libpam_kanidm.so", "usr/lib/x86_64-linux-gnu/security/pam_kanidm.so", "644"],
|
||||
[ "debian/kanidm.pam", "usr/share/pam-configs/kanidm", "644"],
|
||||
[
|
||||
"target/release/libpam_kanidm.so",
|
||||
"usr/lib/x86_64-linux-gnu/security/pam_kanidm.so",
|
||||
"644",
|
||||
],
|
||||
[
|
||||
"debian/kanidm.pam",
|
||||
"usr/share/pam-configs/kanidm",
|
||||
"644",
|
||||
],
|
||||
]
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["pkg-config"]
|
||||
|
|
|
@ -52,7 +52,7 @@ test = true
|
|||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
async-trait = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
dialoguer = { workspace = true }
|
||||
|
@ -89,8 +89,8 @@ uuid = { workspace = true }
|
|||
walkdir = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
sd-notify.workspace = true
|
||||
prctl.workspace = true
|
||||
sd-notify = { workspace = true }
|
||||
prctl = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_family = "windows"))'.dependencies]
|
||||
kanidm_utils_users = { workspace = true }
|
||||
|
@ -135,6 +135,9 @@ assets = [
|
|||
]
|
||||
maintainer-scripts = "debian/"
|
||||
systemd-units = [
|
||||
{ unit-name = "kanidm-unixd", enable = true},
|
||||
{ unit-name = "kanidm-unixd", enable = true},
|
||||
{ unit-name = "kanidm-unixd-tasks", enable = true},
|
||||
]
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["kanidm_build_profiles", "clap_complete"]
|
||||
|
|
|
@ -16,17 +16,55 @@ extern crate tracing;
|
|||
use std::process::ExitCode;
|
||||
|
||||
use clap::Parser;
|
||||
use kanidm_unix_common::client::call_daemon;
|
||||
use kanidm_unix_common::client::DaemonClient;
|
||||
use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH;
|
||||
use kanidm_unix_common::unix_config::KanidmUnixdConfig;
|
||||
use kanidm_unix_common::unix_proto::{
|
||||
ClientRequest, ClientResponse, PamAuthRequest, PamAuthResponse, PamServiceInfo,
|
||||
};
|
||||
// use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
include!("../opt/tool.rs");
|
||||
|
||||
macro_rules! setup_client {
|
||||
() => {{
|
||||
let Ok(cfg) =
|
||||
KanidmUnixdConfig::new().read_options_from_optional_config(DEFAULT_CONFIG_PATH)
|
||||
else {
|
||||
error!("Failed to parse {}", DEFAULT_CONFIG_PATH);
|
||||
return ExitCode::FAILURE;
|
||||
};
|
||||
|
||||
debug!("Connecting to resolver ...");
|
||||
|
||||
debug!(
|
||||
"Using kanidm_unixd socket path: {:?}",
|
||||
cfg.sock_path.as_str()
|
||||
);
|
||||
|
||||
// see if the kanidm_unixd socket exists and quit if not
|
||||
if !PathBuf::from(&cfg.sock_path).exists() {
|
||||
error!(
|
||||
"Failed to find unix socket at {}, quitting!",
|
||||
cfg.sock_path.as_str()
|
||||
);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
match DaemonClient::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout).await {
|
||||
Ok(dc) => dc,
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Failed to connect to resolver at {}-> {:?}",
|
||||
cfg.sock_path.as_str(),
|
||||
err
|
||||
);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> ExitCode {
|
||||
let opt = KanidmUnixParser::parse();
|
||||
|
@ -54,12 +92,7 @@ async fn main() -> ExitCode {
|
|||
} => {
|
||||
debug!("Starting PAM auth tester tool ...");
|
||||
|
||||
let Ok(cfg) =
|
||||
KanidmUnixdConfig::new().read_options_from_optional_config(DEFAULT_CONFIG_PATH)
|
||||
else {
|
||||
error!("Failed to parse {}", DEFAULT_CONFIG_PATH);
|
||||
return ExitCode::FAILURE;
|
||||
};
|
||||
let mut daemon_client = setup_client!();
|
||||
|
||||
info!("Sending request for user {}", &account_id);
|
||||
|
||||
|
@ -72,7 +105,7 @@ async fn main() -> ExitCode {
|
|||
},
|
||||
};
|
||||
loop {
|
||||
match call_daemon(cfg.sock_path.as_str(), req, cfg.unix_sock_timeout).await {
|
||||
match daemon_client.call(&req, None).await {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Success) => {
|
||||
println!("auth success!");
|
||||
|
@ -90,7 +123,7 @@ async fn main() -> ExitCode {
|
|||
ClientResponse::PamAuthenticateStepResponse(PamAuthResponse::Password) => {
|
||||
// Prompt for and get the password
|
||||
let cred = match dialoguer::Password::new()
|
||||
.with_prompt("Enter Unix password: ")
|
||||
.with_prompt("Enter Unix password")
|
||||
.interact()
|
||||
{
|
||||
Ok(p) => p,
|
||||
|
@ -133,7 +166,7 @@ async fn main() -> ExitCode {
|
|||
|
||||
let sereq = ClientRequest::PamAccountAllowed(account_id);
|
||||
|
||||
match call_daemon(cfg.sock_path.as_str(), sereq, cfg.unix_sock_timeout).await {
|
||||
match daemon_client.call(&sereq, None).await {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::PamStatus(Some(true)) => {
|
||||
println!("account success!");
|
||||
|
@ -158,15 +191,7 @@ async fn main() -> ExitCode {
|
|||
KanidmUnixOpt::CacheClear { debug: _, really } => {
|
||||
debug!("Starting cache clear tool ...");
|
||||
|
||||
let cfg = match KanidmUnixdConfig::new()
|
||||
.read_options_from_optional_config(DEFAULT_CONFIG_PATH)
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(_e) => {
|
||||
error!("Failed to parse {}", DEFAULT_CONFIG_PATH);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
let mut daemon_client = setup_client!();
|
||||
|
||||
if !really {
|
||||
error!("Are you sure you want to proceed? If so use --really");
|
||||
|
@ -175,7 +200,7 @@ async fn main() -> ExitCode {
|
|||
|
||||
let req = ClientRequest::ClearCache;
|
||||
|
||||
match call_daemon(cfg.sock_path.as_str(), req, cfg.unix_sock_timeout).await {
|
||||
match daemon_client.call(&req, None).await {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::Ok => info!("success"),
|
||||
_ => {
|
||||
|
@ -192,19 +217,11 @@ async fn main() -> ExitCode {
|
|||
KanidmUnixOpt::CacheInvalidate { debug: _ } => {
|
||||
debug!("Starting cache invalidate tool ...");
|
||||
|
||||
let cfg = match KanidmUnixdConfig::new()
|
||||
.read_options_from_optional_config(DEFAULT_CONFIG_PATH)
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(_e) => {
|
||||
error!("Failed to parse {}", DEFAULT_CONFIG_PATH);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
let mut daemon_client = setup_client!();
|
||||
|
||||
let req = ClientRequest::InvalidateCache;
|
||||
|
||||
match call_daemon(cfg.sock_path.as_str(), req, cfg.unix_sock_timeout).await {
|
||||
match daemon_client.call(&req, None).await {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::Ok => info!("success"),
|
||||
_ => {
|
||||
|
@ -221,43 +238,26 @@ async fn main() -> ExitCode {
|
|||
KanidmUnixOpt::Status { debug: _ } => {
|
||||
trace!("Starting cache status tool ...");
|
||||
|
||||
let cfg = match KanidmUnixdConfig::new()
|
||||
.read_options_from_optional_config(DEFAULT_CONFIG_PATH)
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(_e) => {
|
||||
error!("Failed to parse {}", DEFAULT_CONFIG_PATH);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
let mut daemon_client = setup_client!();
|
||||
let req = ClientRequest::Status;
|
||||
|
||||
let spath = PathBuf::from(cfg.sock_path.as_str());
|
||||
if !spath.exists() {
|
||||
error!(
|
||||
"kanidm_unixd socket {} does not exist - is the service running?",
|
||||
cfg.sock_path
|
||||
)
|
||||
} else {
|
||||
match call_daemon(cfg.sock_path.as_str(), req, cfg.unix_sock_timeout).await {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::ProviderStatus(results) => {
|
||||
for provider in results {
|
||||
println!(
|
||||
"{}: {}",
|
||||
provider.name,
|
||||
if provider.online { "online" } else { "offline" }
|
||||
);
|
||||
}
|
||||
match daemon_client.call(&req, None).await {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::ProviderStatus(results) => {
|
||||
for provider in results {
|
||||
println!(
|
||||
"{}: {}",
|
||||
provider.name,
|
||||
if provider.online { "online" } else { "offline" }
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
error!("Error: unexpected response -> {:?}", r);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error -> {:?}", e);
|
||||
}
|
||||
_ => {
|
||||
error!("Error: unexpected response -> {:?}", r);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error -> {:?}", e);
|
||||
}
|
||||
}
|
||||
ExitCode::SUCCESS
|
||||
|
|
|
@ -17,7 +17,7 @@ use std::path::PathBuf;
|
|||
use std::process::ExitCode;
|
||||
|
||||
use clap::Parser;
|
||||
use kanidm_unix_common::client::call_daemon;
|
||||
use kanidm_unix_common::client::DaemonClient;
|
||||
use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH;
|
||||
use kanidm_unix_common::unix_config::KanidmUnixdConfig;
|
||||
use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse};
|
||||
|
@ -66,21 +66,37 @@ async fn main() -> ExitCode {
|
|||
);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
let mut daemon_client =
|
||||
match DaemonClient::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout).await {
|
||||
Ok(dc) => dc,
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Failed to connect to resolver at {}-> {:?}",
|
||||
cfg.sock_path.as_str(),
|
||||
err
|
||||
);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
// safe because we've already thrown an error if it's not there
|
||||
let req = ClientRequest::SshKey(opt.account_id.unwrap_or("".to_string()));
|
||||
|
||||
match call_daemon(cfg.sock_path.as_str(), req, cfg.unix_sock_timeout).await {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::SshKeys(sk) => sk.iter().for_each(|k| {
|
||||
match daemon_client.call(&req, None).await {
|
||||
Ok(ClientResponse::SshKeys(sk)) => {
|
||||
sk.iter().for_each(|k| {
|
||||
println!("{}", k);
|
||||
}),
|
||||
_ => {
|
||||
error!("Error calling kanidm_unixd: unexpected response -> {:?}", r);
|
||||
}
|
||||
},
|
||||
});
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Ok(r) => {
|
||||
error!("Error calling kanidm_unixd: unexpected response -> {:?}", r);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error calling kanidm_unixd -> {:?}", e);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
};
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ use crate::idprovider::system::{
|
|||
Shadow, SystemAuthResult, SystemProvider, SystemProviderAuthInit, SystemProviderSession,
|
||||
};
|
||||
use crate::unix_config::{HomeAttr, UidAttr};
|
||||
use kanidm_unix_common::constants::DEFAULT_SHELL_SEARCH_PATHS;
|
||||
use kanidm_unix_common::unix_passwd::{EtcGroup, EtcShadow, EtcUser};
|
||||
use kanidm_unix_common::unix_proto::{
|
||||
HomeDirectoryInfo, NssGroup, NssUser, PamAuthRequest, PamAuthResponse, PamServiceInfo,
|
||||
|
@ -331,26 +332,65 @@ impl Resolver {
|
|||
})?;
|
||||
|
||||
// Check if requested `shell` exists on the system, else use `default_shell`
|
||||
let requested_shell_exists: bool = token
|
||||
.shell
|
||||
.as_ref()
|
||||
.map(|shell| {
|
||||
let exists = Path::new(shell).canonicalize()
|
||||
.map_err(|err|{
|
||||
debug!("Failed to canonicalize path, using base path. Tried: {} Error: {:?}", shell, err);
|
||||
}).unwrap_or(Path::new(shell).to_path_buf()).exists();
|
||||
if !exists {
|
||||
warn!(
|
||||
"Configured shell for {} is not present on this system - {}. Check `/etc/shells` for valid shell options.", token.name,
|
||||
shell
|
||||
)
|
||||
let maybe_shell = token.shell.as_ref().map(PathBuf::from);
|
||||
|
||||
let requested_shell_exists = if let Some(shell_path) = maybe_shell.as_ref() {
|
||||
// Does the shell path as configured exist?
|
||||
let mut exists = shell_path
|
||||
.canonicalize()
|
||||
.map_err(|err| {
|
||||
debug!(
|
||||
"Failed to canonicalize path, using base path. Tried: {} Error: {:?}",
|
||||
shell_path.to_string_lossy(),
|
||||
err
|
||||
);
|
||||
})
|
||||
.unwrap_or(Path::new(shell_path).to_path_buf())
|
||||
.exists();
|
||||
|
||||
if !exists {
|
||||
// Does the shell binary exist in a search path that is configured?
|
||||
if let Some(shell_binary_name) = shell_path.file_name() {
|
||||
for search_path in DEFAULT_SHELL_SEARCH_PATHS {
|
||||
//
|
||||
let shell_path = Path::new(search_path).join(shell_binary_name);
|
||||
if shell_path.exists() {
|
||||
// Okay, the binary name exists but in an alternate path. This can
|
||||
// commonly occur with freebsd where the shell may be installed
|
||||
// in /usr/local/bin instead of /bin.
|
||||
//
|
||||
// This could also occur if the user configured the shell as "zsh"
|
||||
// rather than an absolute path.
|
||||
let Some(shell_path_utf8) = shell_path.to_str().map(String::from)
|
||||
else {
|
||||
warn!("Configured shell \"{}\" for {} was found but the complete path is not valid utf-8 and can not be used.",
|
||||
shell_binary_name.to_string_lossy(), token.name);
|
||||
continue;
|
||||
};
|
||||
|
||||
// Update the path
|
||||
token.shell = Some(shell_path_utf8);
|
||||
// We exist
|
||||
exists = true;
|
||||
// No need to loop any more
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
exists
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
info!("User has not specified a shell, using default");
|
||||
false
|
||||
});
|
||||
}
|
||||
|
||||
if !exists {
|
||||
warn!(
|
||||
"Configured shell \"{}\" for {} is not present on this system. Check `/etc/shells` for valid shell options.",
|
||||
shell_path.to_string_lossy(), token.name
|
||||
)
|
||||
}
|
||||
|
||||
exists
|
||||
} else {
|
||||
info!("User has not specified a shell, using default");
|
||||
false
|
||||
};
|
||||
|
||||
if !requested_shell_exists {
|
||||
token.shell = Some(self.default_shell.clone())
|
||||
|
|
Loading…
Reference in a new issue