Compare commits

..

1 commit

Author SHA1 Message Date
Firstyear 6f5571e49f
Merge ee46216093 into e838da9a08 2025-02-13 11:36:09 -05:00
17 changed files with 1007 additions and 498 deletions

View file

@ -35,15 +35,9 @@ jobs:
needs:
- set_tag_values
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/checkout@v4
- 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:
@ -53,9 +47,6 @@ 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
@ -69,8 +60,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' )
needs: [kanidm_build, set_tag_values]
if: ( github.ref_type == 'tag' || github.ref == 'refs/heads/master' ) && github.repository == 'kanidm/kanidm'
needs: kanidm_build
permissions:
packages: write
@ -87,4 +78,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/${{ needs.set_tag_values.outputs.owner_lc }}/kanidm:devel"
"ghcr.io/${{ github.repository_owner }}/kanidm:devel"

View file

@ -35,15 +35,27 @@ jobs:
runs-on: ubuntu-latest
needs: set_tag_values
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/checkout@v4
- 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:
@ -52,9 +64,6 @@ 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
@ -68,8 +77,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' )
needs: [kanidmd_build, set_tag_values]
if: ( github.ref_type== 'tag' || github.ref == 'refs/heads/master' ) && github.repository == 'kanidm/kanidm'
needs: kanidmd_build
permissions:
packages: write
@ -86,4 +95,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/${{ needs.set_tag_values.outputs.owner_lc }}/kanidmd:devel"
"ghcr.io/${{ github.repository_owner }}/kanidmd:devel"

View file

@ -35,26 +35,17 @@ jobs:
runs-on: ubuntu-latest
needs: set_tag_values
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/checkout@v4
- 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
@ -68,8 +59,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' )
needs: [radius_build, set_tag_values]
if: ( github.ref_type == 'tag' || github.ref == 'refs/heads/master' ) && github.repository == 'kanidm/kanidm'
needs: radius_build
permissions:
packages: write
@ -88,4 +79,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/${{ needs.set_tag_values.outputs.owner_lc }}/radius:devel"
"ghcr.io/${{ github.repository_owner }}/radius:devel"

View file

@ -215,7 +215,7 @@ Token signing public key
### Create the Kanidm Configuration
By default, members of the `idm_admins` or `idm_oauth2_admins` groups are able to create
By default, members of the `system_admins` or `idm_hp_oauth2_manage_priv` groups are able to create
or manage OAuth2 client integrations.
You can create a new client by specifying its client name, application display name and the landing

View file

@ -556,65 +556,6 @@ php occ config:app:set --value=0 user_oidc allow_multiple_user_backends
You can login directly by appending `?direct=1` to your login page. You can re-enable other backends
by setting the value to `1`
## OAuth2 Proxy
OAuth2 Proxy is a reverse proxy that provides authentication with OpenID Connect identity providers.
It is typically used to secure web applications without native OpenID Connect support.
Prepare the environment.
Due to a [lack of public client support](https://github.com/oauth2-proxy/oauth2-proxy/issues/1714) we have to set it up as a basic client.
```bash
kanidm system oauth2 create webapp 'webapp.example.com' 'https://webapp.example.com'
kanidm system add-redirect-url webapp 'https://webapp.example.com/oauth2/callback'
kanidm system oauth2 update-scope-map webapp email openid
kanidm system oauth2 get webapp
kanidm system oauth2 show-basic-secret webapp
<SECRET>
```
Create a user group.
```bash
kanidm group create 'webapp_admin'
```
Setup the claim-map to add `webapp_group` to the userinfo claim.
```bash
kanidm system oauth2 update-claim-map-join 'webapp' 'webapp_group' array
kanidm system oauth2 update-claim-map 'webapp' 'webapp_group' 'webapp_admin' 'webapp_admin'
```
Authorize users for the application.
Additionally OAuth2 Proxy requires all users have an email, reference this issue for more details:
- <https://github.com/oauth2-proxy/oauth2-proxy/issues/2667>
```bash
kanidm person update '<user>' --legalname 'Personal Name' --mail 'user@example.com'
kanidm group add-members 'webapp_admin' '<user>'
```
And add the following to your OAuth2 Proxy config.
```toml
provider = "oidc"
scope = "openid email"
# change to match your kanidm domain and client id
oidc_issuer_url = "https://idm.example.com/oauth2/openid/webapp"
# client ID from `kanidm system oauth2 create`
client_id = "webapp"
# redirect URL from `kanidm system add-redirect-url webapp`
redirect_url = "https://webapp.example.com/oauth2/callback"
# claim name from `kanidm system oauth2 update-claim-map-join`
oidc_groups_claim = "webapp_group"
# user group from `kanidm group create`
allowed_groups = ["webapp_admin"]
# secret from `kanidm system oauth2 show-basic-secret webapp`
client_secret = "<SECRET>"
```
## Outline
> These instructions were tested with self-hosted Outline 0.80.2.

View file

@ -22,7 +22,6 @@ This is a list of supported features and standards within Kanidm.
- [RFC4519 LDAP Schema](https://www.rfc-editor.org/rfc/rfc4519)
- FreeIPA User Schema
- [RFC7644 SCIM Bulk Data Import](https://www.rfc-editor.org/rfc/rfc7644)
- NOTE: SCIM is only supported for synchronisation from another IDP at this time.
# Database

View file

@ -289,6 +289,15 @@ 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",
@ -364,7 +373,36 @@ 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_DL8: BuiltinGroup = BuiltinGroup {
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 {
name: "idm_high_privilege",
uuid: UUID_IDM_HIGH_PRIVILEGE,
entry_managed_by: Some(UUID_IDM_ACCESS_CONTROL_ADMINS),
@ -388,14 +426,12 @@ 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_DL8: BuiltinGroup = BuiltinGroup {
pub static ref BUILTIN_GROUP_APPLICATION_ADMINS: BuiltinGroup = BuiltinGroup {
name: "idm_application_admins",
uuid: UUID_IDM_APPLICATION_ADMINS,
description: "Builtin Application Administration Group.",
@ -422,19 +458,17 @@ 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,
&BUILTIN_GROUP_PEOPLE_SELF_NAME_WRITE_DL7,
&IDM_PEOPLE_SELF_MAIL_WRITE_DL7,
&BUILTIN_GROUP_CLIENT_CERTIFICATE_ADMINS_DL7,
&BUILTIN_GROUP_APPLICATION_ADMINS_DL8,
&IDM_PEOPLE_SELF_WRITE_MAIL_V1,
// Write deps on read, so write must be added first.
// All members must exist before we write HP
&IDM_HIGH_PRIVILEGE_DL8,
&IDM_HIGH_PRIVILEGE_V1,
// other things
&IDM_UI_ENABLE_EXPERIMENTAL_FEATURES,
&IDM_ACCOUNT_MAIL_READ,

View file

@ -54,6 +54,14 @@ 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;
@ -77,7 +85,7 @@ 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_8;
pub const DOMAIN_MIN_REMIGRATION_LEVEL: DomainVersion = DOMAIN_TGT_LEVEL - 2;
// The minimum supported domain functional level (for replication)
pub const DOMAIN_MIN_LEVEL: DomainVersion = DOMAIN_TGT_LEVEL;
// The previous releases domain functional level

View file

@ -208,16 +208,6 @@ pub static ref SCHEMA_ATTR_DENIED_NAME: SchemaAttribute = SchemaAttribute {
..Default::default()
};
pub static ref SCHEMA_ATTR_DENIED_NAME_DL10: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DENIED_NAME,
name: Attribute::DeniedName,
description: "Iname values that are not allowed to be used in 'name'.".to_string(),
syntax: SyntaxType::Utf8StringIname,
multivalue: true,
..Default::default()
};
pub static ref SCHEMA_ATTR_DOMAIN_TOKEN_KEY: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DOMAIN_TOKEN_KEY,
name: Attribute::DomainTokenKey,
@ -1219,30 +1209,6 @@ 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(),

View file

@ -5,7 +5,7 @@ use base64::{
Engine as _,
};
use compact_jwt::{Jws, JwsCompact};
use compact_jwt::{Jws, JwsCompact, JwsEs256Signer, JwsSigner};
use kanidm_proto::internal::{ApiTokenPurpose, ScimSyncToken};
use kanidm_proto::scim_v1::*;
use std::collections::{BTreeMap, BTreeSet};
@ -25,6 +25,7 @@ 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 {
@ -39,6 +40,15 @@ 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()
@ -50,6 +60,7 @@ macro_rules! try_from_entry {
name,
uuid,
sync_tokens,
jws_key,
})
}};
}
@ -112,6 +123,16 @@ 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;
@ -164,9 +185,25 @@ impl IdmServerProxyWriteTransaction<'_> {
})?;
// The modify succeeded and was allowed, now sign the token for return.
self.qs_write
.get_domain_key_object_handle()?
.jws_es256_sign(&token, ct)
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)
}
// Done!
}

View file

@ -1,7 +1,7 @@
use std::collections::BTreeMap;
use std::time::Duration;
use compact_jwt::{Jws, JwsCompact};
use compact_jwt::{Jws, JwsCompact, JwsEs256Signer, JwsSigner};
use kanidm_proto::internal::ApiToken as ProtoApiToken;
use time::OffsetDateTime;
@ -23,6 +23,15 @@ 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()
@ -39,6 +48,7 @@ macro_rules! try_from_entry {
valid_from,
expire,
api_tokens,
jws_key,
})
}};
}
@ -50,6 +60,8 @@ pub struct ServiceAccount {
pub expire: Option<OffsetDateTime>,
pub api_tokens: BTreeMap<Uuid, ApiToken>,
pub jws_key: Option<JwsEs256Signer>,
}
impl ServiceAccount {
@ -241,9 +253,25 @@ impl IdmServerProxyWriteTransaction<'_> {
err
})?;
self.qs_write
.get_domain_key_object_handle()?
.jws_es256_sign(&token, ct)
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)
}
}
pub fn service_account_destroy_api_token(

View file

@ -7,6 +7,8 @@
use std::iter::once;
use std::sync::Arc;
use compact_jwt::JwsEs256Signer;
use rand::prelude::*;
use regex::Regex;
use tracing::trace;
@ -59,6 +61,13 @@ 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>(
@ -70,14 +79,11 @@ 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);
}
}
@ -103,26 +109,39 @@ impl Domain {
debug!("plugin_domain: NOT Applying domain version transform");
};
// 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
);
// 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);
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(())

View file

@ -62,7 +62,10 @@ pub const GID_UNUSED_D_MAX: u32 = 0x7fff_ffff;
pub struct GidNumber {}
fn apply_gidnumber<T: Clone>(e: &mut Entry<EntryInvalid, T>) -> Result<(), OperationError> {
fn apply_gidnumber<T: Clone>(
e: &mut Entry<EntryInvalid, T>,
domain_version: DomainVersion,
) -> Result<(), OperationError> {
if (e.attribute_equality(Attribute::Class, &EntryClass::PosixGroup.into())
|| e.attribute_equality(Attribute::Class, &EntryClass::PosixAccount.into()))
&& !e.attribute_pres(Attribute::GidNumber)
@ -86,33 +89,48 @@ fn apply_gidnumber<T: Clone>(e: &mut Entry<EntryInvalid, T>) -> Result<(), Opera
e.set_ava(&Attribute::GidNumber, once(gid_v));
Ok(())
} else if let Some(gid) = e.get_ava_single_uint32(Attribute::GidNumber) {
// 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(())
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(())
}
} 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)
// 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)
}
}
} else {
Ok(())
@ -126,31 +144,37 @@ 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> {
cand.iter_mut().try_for_each(apply_gidnumber)
let dv = qs.get_domain_version();
cand.iter_mut()
.try_for_each(|cand| apply_gidnumber(cand, dv))
}
#[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> {
cand.iter_mut().try_for_each(apply_gidnumber)
let dv = qs.get_domain_version();
cand.iter_mut()
.try_for_each(|cand| apply_gidnumber(cand, dv))
}
#[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> {
cand.iter_mut().try_for_each(apply_gidnumber)
let dv = qs.get_domain_version();
cand.iter_mut()
.try_for_each(|cand| apply_gidnumber(cand, dv))
}
}
@ -162,7 +186,9 @@ mod tests {
};
use crate::prelude::*;
#[qs_test]
use kanidm_proto::internal::DomainUpgradeCheckStatus as ProtoDomainUpgradeCheckStatus;
#[qs_test(domain_level=DOMAIN_LEVEL_7)]
async fn test_gidnumber_generate(server: &QueryServer) {
let mut server_txn = server.write(duration_from_epoch_now()).await.expect("txn");
@ -397,4 +423,85 @@ 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());
}
}

View file

@ -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,6 +88,20 @@ 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(())
})
}

View file

@ -10,7 +10,7 @@ impl Plugin for ValueDeny {
"plugin_value_deny"
}
#[instrument(level = "debug", name = "denied_names_pre_create_transform", skip_all)]
#[instrument(level = "debug", name = "base_pre_create_transform", skip_all)]
#[allow(clippy::cognitive_complexity)]
fn pre_create_transform(
qs: &mut QueryServerWriteTransaction,
@ -19,25 +19,9 @@ impl Plugin for ValueDeny {
) -> Result<(), OperationError> {
let denied_names = qs.denied_names();
if denied_names.is_empty() {
// Nothing to check.
return Ok(());
}
let mut pass = true;
for entry in cand {
// If the entry doesn't have a uuid, it's invalid anyway and will fail schema.
if let Some(e_uuid) = entry.get_uuid() {
// SAFETY - Thanks to JpWarren blowing his nipper clean off, we need to
// assert that the break glass and system accounts are NOT subject to
// this process.
if e_uuid < DYNAMIC_RANGE_MINIMUM_UUID {
// These entries are exempt
continue;
}
}
if let Some(name) = entry.get_ava_single_iname(Attribute::Name) {
if denied_names.contains(name) {
pass = false;
@ -53,24 +37,27 @@ impl Plugin for ValueDeny {
}
}
#[instrument(level = "debug", name = "base_pre_modify", skip_all)]
fn pre_modify(
qs: &mut QueryServerWriteTransaction,
pre_cand: &[Arc<EntrySealedCommitted>],
_pre_cand: &[Arc<EntrySealedCommitted>],
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
_me: &ModifyEvent,
) -> Result<(), OperationError> {
Self::modify(qs, pre_cand, cand)
Self::modify(qs, cand)
}
#[instrument(level = "debug", name = "base_pre_modify", skip_all)]
fn pre_batch_modify(
qs: &mut QueryServerWriteTransaction,
pre_cand: &[Arc<EntrySealedCommitted>],
_pre_cand: &[Arc<EntrySealedCommitted>],
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
_me: &BatchModifyEvent,
) -> Result<(), OperationError> {
Self::modify(qs, pre_cand, cand)
Self::modify(qs, cand)
}
#[instrument(level = "debug", name = "base::verify", skip_all)]
fn verify(qs: &mut QueryServerReadTransaction) -> Vec<Result<(), ConsistencyError>> {
let denied_names = qs.denied_names().clone();
@ -81,15 +68,7 @@ impl Plugin for ValueDeny {
match qs.internal_search(filt) {
Ok(entries) => {
for entry in entries {
let e_uuid = entry.get_uuid();
// SAFETY - Thanks to JpWarren blowing his nipper clean off, we need to
// assert that the break glass accounts are NOT subject to this process.
if e_uuid < DYNAMIC_RANGE_MINIMUM_UUID {
// These entries are exempt
continue;
}
results.push(Err(ConsistencyError::DeniedName(e_uuid)));
results.push(Err(ConsistencyError::DeniedName(entry.get_uuid())));
}
}
Err(err) => {
@ -104,37 +83,17 @@ impl Plugin for ValueDeny {
}
impl ValueDeny {
#[instrument(level = "debug", name = "denied_names_modify", skip_all)]
fn modify(
qs: &mut QueryServerWriteTransaction,
pre_cand: &[Arc<EntrySealedCommitted>],
cand: &mut [EntryInvalidCommitted],
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
) -> Result<(), OperationError> {
let denied_names = qs.denied_names();
if denied_names.is_empty() {
// Nothing to check.
return Ok(());
}
let mut pass = true;
for (pre_entry, post_entry) in pre_cand.iter().zip(cand.iter()) {
// If the entry doesn't have a uuid, it's invalid anyway and will fail schema.
let e_uuid = pre_entry.get_uuid();
// SAFETY - Thanks to JpWarren blowing his nipper clean off, we need to
// assert that the break glass accounts are NOT subject to this process.
if e_uuid < DYNAMIC_RANGE_MINIMUM_UUID {
// These entries are exempt
continue;
}
let pre_name = pre_entry.get_ava_single_iname(Attribute::Name);
let post_name = post_entry.get_ava_single_iname(Attribute::Name);
if let Some(name) = post_name {
// Only if the name is changing, and is denied.
if pre_name != post_name && denied_names.contains(name) {
for entry in cand {
if let Some(name) = entry.get_ava_single_iname(Attribute::Name) {
if denied_names.contains(name) {
pass = false;
error!(?name, "name denied by system configuration");
}
@ -158,10 +117,10 @@ mod tests {
let me_inv_m = ModifyEvent::new_internal_invalid(
filter!(f_eq(Attribute::Uuid, PVUUID_SYSTEM_CONFIG.clone())),
ModifyList::new_list(vec![
Modify::Present(Attribute::DeniedName, Value::new_iname("tobias")),
Modify::Present(Attribute::DeniedName, Value::new_iname("ellie")),
]),
ModifyList::new_list(vec![Modify::Present(
Attribute::DeniedName,
Value::new_iname("tobias"),
)]),
);
assert!(server_txn.modify(&me_inv_m).is_ok());
@ -189,103 +148,30 @@ mod tests {
#[qs_test]
async fn test_valuedeny_modify(server: &QueryServer) {
// Create an entry that has a name which will become denied to test how it
// interacts.
setup_name_deny(server).await;
let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
let e_uuid = Uuid::new_v4();
let t_uuid = Uuid::new_v4();
assert!(server_txn
.internal_create(vec![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("ellie")),
(Attribute::Uuid, Value::Uuid(e_uuid)),
(Attribute::Description, Value::new_utf8s("Ellie Meow")),
(Attribute::DisplayName, Value::new_utf8s("Ellie Meow"))
(Attribute::Name, Value::new_iname("newname")),
(Attribute::Uuid, Value::Uuid(t_uuid)),
(Attribute::Description, Value::new_utf8s("Tobias")),
(Attribute::DisplayName, Value::new_utf8s("Tobias"))
),])
.is_ok());
assert!(server_txn.commit().is_ok());
// Now mod it
setup_name_deny(server).await;
let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
// Attempt to mod ellie.
// Can mod a different attribute
assert!(server_txn
.internal_modify_uuid(
e_uuid,
&ModifyList::new_purge_and_set(Attribute::DisplayName, Value::new_utf8s("tobias"))
)
.is_ok());
// Can't mod to another invalid name.
assert!(server_txn
.internal_modify_uuid(
e_uuid,
t_uuid,
&ModifyList::new_purge_and_set(Attribute::Name, Value::new_iname("tobias"))
)
.is_err());
// Can mod to a valid name.
assert!(server_txn
.internal_modify_uuid(
e_uuid,
&ModifyList::new_purge_and_set(
Attribute::Name,
Value::new_iname("miss_meowington")
)
)
.is_ok());
// Now mod from the valid name to an invalid one.
assert!(server_txn
.internal_modify_uuid(
e_uuid,
&ModifyList::new_purge_and_set(Attribute::Name, Value::new_iname("tobias"))
)
.is_err());
assert!(server_txn.commit().is_ok());
}
#[qs_test]
async fn test_valuedeny_jpwarren_special(server: &QueryServer) {
// Assert that our break glass accounts are exempt from this processing.
let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
let me_inv_m = ModifyEvent::new_internal_invalid(
filter!(f_eq(Attribute::Uuid, PVUUID_SYSTEM_CONFIG.clone())),
ModifyList::new_list(vec![
Modify::Present(Attribute::DeniedName, Value::new_iname("admin")),
Modify::Present(Attribute::DeniedName, Value::new_iname("idm_admin")),
]),
);
assert!(server_txn.modify(&me_inv_m).is_ok());
assert!(server_txn.commit().is_ok());
let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
assert!(server_txn
.internal_modify_uuid(
UUID_IDM_ADMIN,
&ModifyList::new_purge_and_set(
Attribute::DisplayName,
Value::new_utf8s("Idm Admin")
)
)
.is_ok());
assert!(server_txn
.internal_modify_uuid(
UUID_ADMIN,
&ModifyList::new_purge_and_set(Attribute::DisplayName, Value::new_utf8s("Admin"))
)
.is_ok());
assert!(server_txn.commit().is_ok());
}
#[qs_test]

View file

@ -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_patch_level < DOMAIN_TGT_PATCH_LEVEL {
if domain_target_level >= DOMAIN_LEVEL_7 && domain_patch_level < DOMAIN_TGT_PATCH_LEVEL {
write_txn
.internal_modify_uuid(
UUID_DOMAIN_INFO,
@ -294,6 +294,346 @@ 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> {
@ -424,24 +764,6 @@ impl QueryServerWriteTransaction<'_> {
return Err(OperationError::MG0004DomainLevelInDevelopment);
}
// =========== Apply changes ==============
// Now update schema
let idm_schema_changes = [
SCHEMA_ATTR_DENIED_NAME_DL10.clone().into(),
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(())
}
@ -506,7 +828,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(),
@ -516,7 +838,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(),
@ -531,7 +853,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(),
@ -545,7 +867,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(),
@ -580,17 +902,6 @@ 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
@ -617,14 +928,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(),
@ -632,18 +943,7 @@ 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(),
// 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(),
SCHEMA_CLASS_DOMAIN_INFO_DL6.clone().into(),
];
let r: Result<(), _> = idm_schema_classes_dl1
@ -734,10 +1034,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(),
@ -759,26 +1059,13 @@ 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(),
// 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(),
IDM_ACP_DOMAIN_ADMIN_DL6.clone(),
];
let res: Result<(), _> = idm_entries
@ -808,6 +1095,19 @@ 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()
@ -841,6 +1141,94 @@ 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> {
@ -912,7 +1300,7 @@ impl QueryServerReadTransaction<'_> {
#[cfg(test)]
mod tests {
// use super::{ProtoDomainUpgradeCheckItem, ProtoDomainUpgradeCheckStatus};
use super::{ProtoDomainUpgradeCheckItem, ProtoDomainUpgradeCheckStatus};
use crate::prelude::*;
#[qs_test]
@ -941,8 +1329,9 @@ mod tests {
}
}
#[qs_test(domain_level=DOMAIN_LEVEL_8)]
async fn test_migrations_dl8_dl9(server: &QueryServer) {
#[qs_test(domain_level=DOMAIN_LEVEL_6)]
async fn test_migrations_dl6_dl7(server: &QueryServer) {
// Assert our instance was setup to version 6
let mut write_txn = server.write(duration_from_epoch_now()).await.unwrap();
let db_domain_version = write_txn
@ -951,95 +1340,167 @@ mod tests {
.get_ava_single_uint32(Attribute::Version)
.expect("Attribute Version not present");
assert_eq!(db_domain_version, DOMAIN_LEVEL_8);
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");
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();
// drop(read_txn);
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 write_txn = server.write(duration_from_epoch_now()).await.unwrap();
// Fix any issues
// == Increase the version ==
write_txn
.internal_apply_domain_migration(DOMAIN_LEVEL_9)
.expect("Unable to set domain level to version 9");
.internal_modify_uuid(
oauth2_client_uuid,
&ModifyList::new_purge_and_set(
Attribute::OAuth2StrictRedirectUri,
Value::Bool(true),
),
)
.expect("Unable to enforce strict mode.");
// Set the version to 8.
write_txn
.internal_apply_domain_migration(DOMAIN_LEVEL_8)
.expect("Unable to set domain level to version 8");
// 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) {
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");
}
async fn test_migrations_dl9_dl10(_server: &QueryServer) {}
#[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");
}
async fn test_migrations_dl10_dl11(_server: &QueryServer) {}
}

View file

@ -1238,6 +1238,13 @@ 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)
@ -2328,7 +2335,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_MIN_REMIGRATION_LEVEL && previous_version != DOMAIN_LEVEL_0 {
if previous_version <= DOMAIN_LEVEL_5 && 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);
@ -2336,8 +2343,21 @@ 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()?;
}
@ -2346,12 +2366,10 @@ 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()?;
}
@ -2376,7 +2394,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
let display_name = domain_entry
.get_ava_single_utf8(Attribute::DomainDisplayName)
.map(str::to_string)
.unwrap_or_else(|| format!("Kanidm {}", domain_name));
.ok_or(OperationError::InvalidEntryState)?;
let domain_ldap_allow_unix_pw_bind = domain_entry
.get_ava_single_bool(Attribute::LdapAllowUnixPwBind)