20240125 2217 client credentials grant (#2456)

* Huge fix of a replication problem.
* Update test
* Increase min replication level
* Client Credentials Grant implementation
This commit is contained in:
Firstyear 2024-02-01 12:00:29 +10:00 committed by GitHub
parent 492c3da36c
commit d42268269a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 1424 additions and 312 deletions

View file

@ -1,10 +1,10 @@
use crate::{ClientError, KanidmClient}; use crate::{ClientError, KanidmClient};
use kanidm_proto::constants::{ use kanidm_proto::constants::{
ATTR_DISPLAYNAME, ATTR_ES256_PRIVATE_KEY_DER, ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE, ATTR_DISPLAYNAME, ATTR_ES256_PRIVATE_KEY_DER, ATTR_NAME,
ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT, ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE, ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE, ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT,
ATTR_OAUTH2_PREFER_SHORT_USERNAME, ATTR_OAUTH2_RS_BASIC_SECRET, ATTR_OAUTH2_RS_NAME, ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE, ATTR_OAUTH2_PREFER_SHORT_USERNAME,
ATTR_OAUTH2_RS_ORIGIN, ATTR_OAUTH2_RS_ORIGIN_LANDING, ATTR_OAUTH2_RS_TOKEN_KEY, ATTR_OAUTH2_RS_BASIC_SECRET, ATTR_OAUTH2_RS_ORIGIN, ATTR_OAUTH2_RS_ORIGIN_LANDING,
ATTR_RS256_PRIVATE_KEY_DER, ATTR_OAUTH2_RS_TOKEN_KEY, ATTR_RS256_PRIVATE_KEY_DER,
}; };
use kanidm_proto::internal::{ImageValue, Oauth2ClaimMapJoin}; use kanidm_proto::internal::{ImageValue, Oauth2ClaimMapJoin};
use kanidm_proto::v1::Entry; use kanidm_proto::v1::Entry;
@ -27,7 +27,7 @@ impl KanidmClient {
let mut new_oauth2_rs = Entry::default(); let mut new_oauth2_rs = Entry::default();
new_oauth2_rs new_oauth2_rs
.attrs .attrs
.insert(ATTR_OAUTH2_RS_NAME.to_string(), vec![name.to_string()]); .insert(ATTR_NAME.to_string(), vec![name.to_string()]);
new_oauth2_rs new_oauth2_rs
.attrs .attrs
.insert(ATTR_DISPLAYNAME.to_string(), vec![displayname.to_string()]); .insert(ATTR_DISPLAYNAME.to_string(), vec![displayname.to_string()]);
@ -47,7 +47,7 @@ impl KanidmClient {
let mut new_oauth2_rs = Entry::default(); let mut new_oauth2_rs = Entry::default();
new_oauth2_rs new_oauth2_rs
.attrs .attrs
.insert(ATTR_OAUTH2_RS_NAME.to_string(), vec![name.to_string()]); .insert(ATTR_NAME.to_string(), vec![name.to_string()]);
new_oauth2_rs new_oauth2_rs
.attrs .attrs
.insert(ATTR_DISPLAYNAME.to_string(), vec![displayname.to_string()]); .insert(ATTR_DISPLAYNAME.to_string(), vec![displayname.to_string()]);
@ -91,7 +91,7 @@ impl KanidmClient {
if let Some(newname) = name { if let Some(newname) = name {
update_oauth2_rs update_oauth2_rs
.attrs .attrs
.insert(ATTR_OAUTH2_RS_NAME.to_string(), vec![newname.to_string()]); .insert(ATTR_NAME.to_string(), vec![newname.to_string()]);
} }
if let Some(newdisplayname) = displayname { if let Some(newdisplayname) = displayname {
update_oauth2_rs.attrs.insert( update_oauth2_rs.attrs.insert(

View file

@ -145,6 +145,7 @@ pub const ATTR_PRIVATE_COOKIE_KEY: &str = "private_cookie_key";
pub const ATTR_PRIVILEGE_EXPIRY: &str = "privilege_expiry"; pub const ATTR_PRIVILEGE_EXPIRY: &str = "privilege_expiry";
pub const ATTR_RADIUS_SECRET: &str = "radius_secret"; pub const ATTR_RADIUS_SECRET: &str = "radius_secret";
pub const ATTR_RECYCLED: &str = "recycled"; pub const ATTR_RECYCLED: &str = "recycled";
pub const ATTR_RECYCLEDDIRECTMEMBEROF: &str = "recycled_directmemberof";
pub const ATTR_REPLICATED: &str = "replicated"; pub const ATTR_REPLICATED: &str = "replicated";
pub const ATTR_RS256_PRIVATE_KEY_DER: &str = "rs256_private_key_der"; pub const ATTR_RS256_PRIVATE_KEY_DER: &str = "rs256_private_key_der";
pub const ATTR_SCOPE: &str = "scope"; pub const ATTR_SCOPE: &str = "scope";

View file

@ -85,6 +85,10 @@ pub enum GrantTypeReq {
redirect_uri: Url, redirect_uri: Url,
code_verifier: Option<String>, code_verifier: Option<String>,
}, },
ClientCredentials {
#[serde_as(as = "Option<StringWithSeparator::<SpaceSeparator, String>>")]
scope: Option<BTreeSet<String>>,
},
RefreshToken { RefreshToken {
refresh_token: String, refresh_token: String,
#[serde_as(as = "Option<StringWithSeparator::<SpaceSeparator, String>>")] #[serde_as(as = "Option<StringWithSeparator::<SpaceSeparator, String>>")]

View file

@ -10,8 +10,8 @@ class OAuth2Rs(BaseModel):
classes: List[str] classes: List[str]
displayname: str displayname: str
es256_private_key_der: str es256_private_key_der: str
name: str
oauth2_rs_basic_secret: str oauth2_rs_basic_secret: str
oauth2_rs_name: str
oauth2_rs_origin: str oauth2_rs_origin: str
oauth2_rs_token_key: str oauth2_rs_token_key: str
oauth2_rs_sup_scope_map: List[str] oauth2_rs_sup_scope_map: List[str]
@ -27,8 +27,8 @@ class RawOAuth2Rs(BaseModel):
required_fields = ( required_fields = (
"displayname", "displayname",
"es256_private_key_der", "es256_private_key_der",
"name",
"oauth2_rs_basic_secret", "oauth2_rs_basic_secret",
"oauth2_rs_name",
"oauth2_rs_origin", "oauth2_rs_origin",
"oauth2_rs_token_key", "oauth2_rs_token_key",
) )
@ -42,8 +42,8 @@ class RawOAuth2Rs(BaseModel):
classes=self.attrs["class"], classes=self.attrs["class"],
displayname=self.attrs["displayname"][0], displayname=self.attrs["displayname"][0],
es256_private_key_der=self.attrs["es256_private_key_der"][0], es256_private_key_der=self.attrs["es256_private_key_der"][0],
name=self.attrs["name"][0],
oauth2_rs_basic_secret=self.attrs["oauth2_rs_basic_secret"][0], oauth2_rs_basic_secret=self.attrs["oauth2_rs_basic_secret"][0],
oauth2_rs_name=self.attrs["oauth2_rs_name"][0],
oauth2_rs_origin=self.attrs["oauth2_rs_origin"][0], oauth2_rs_origin=self.attrs["oauth2_rs_origin"][0],
oauth2_rs_token_key=self.attrs["oauth2_rs_token_key"][0], oauth2_rs_token_key=self.attrs["oauth2_rs_token_key"][0],
oauth2_rs_sup_scope_map=self.attrs.get("oauth2_rs_sup_scope_map", []), oauth2_rs_sup_scope_map=self.attrs.get("oauth2_rs_sup_scope_map", []),

View file

@ -75,7 +75,7 @@ impl IntoResponse for HTTPOauth2Error {
pub(crate) fn oauth2_id(rs_name: &str) -> Filter<FilterInvalid> { pub(crate) fn oauth2_id(rs_name: &str) -> Filter<FilterInvalid> {
filter_all!(f_and!([ filter_all!(f_and!([
f_eq(Attribute::Class, EntryClass::OAuth2ResourceServer.into()), f_eq(Attribute::Class, EntryClass::OAuth2ResourceServer.into()),
f_eq(Attribute::OAuth2RsName, PartialValue::new_iname(rs_name)) f_eq(Attribute::Name, PartialValue::new_iname(rs_name))
])) ]))
} }

View file

@ -57,6 +57,7 @@ pub(crate) async fn oauth2_basic_post(
let classes = vec![ let classes = vec![
EntryClass::OAuth2ResourceServer.to_string(), EntryClass::OAuth2ResourceServer.to_string(),
EntryClass::OAuth2ResourceServerBasic.to_string(), EntryClass::OAuth2ResourceServerBasic.to_string(),
EntryClass::Account.to_string(),
EntryClass::Object.to_string(), EntryClass::Object.to_string(),
]; ];
json_rest_event_post(state, classes, obj, kopid, client_auth_info).await json_rest_event_post(state, classes, obj, kopid, client_auth_info).await
@ -82,6 +83,7 @@ pub(crate) async fn oauth2_public_post(
let classes = vec![ let classes = vec![
EntryClass::OAuth2ResourceServer.to_string(), EntryClass::OAuth2ResourceServer.to_string(),
EntryClass::OAuth2ResourceServerPublic.to_string(), EntryClass::OAuth2ResourceServerPublic.to_string(),
EntryClass::Account.to_string(),
EntryClass::Object.to_string(), EntryClass::Object.to_string(),
]; ];
json_rest_event_post(state, classes, obj, kopid, client_auth_info).await json_rest_event_post(state, classes, obj, kopid, client_auth_info).await

View file

@ -617,7 +617,7 @@ async fn repl_acceptor(
}; };
// Setup a broadcast to control our tasks. // Setup a broadcast to control our tasks.
let (task_tx, task_rx1) = broadcast::channel(2); let (task_tx, task_rx1) = broadcast::channel(1);
// Note, we drop this task here since each task will re-subscribe. That way the // Note, we drop this task here since each task will re-subscribe. That way the
// broadcast doesn't jam up because we aren't draining this task. // broadcast doesn't jam up because we aren't draining this task.
drop(task_rx1); drop(task_rx1);

View file

@ -544,6 +544,7 @@ pub enum DbValueApiToken {
}, },
} }
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub enum DbValueOauth2Session { pub enum DbValueOauth2Session {
V1 { V1 {
@ -570,6 +571,18 @@ pub enum DbValueOauth2Session {
#[serde(rename = "r")] #[serde(rename = "r")]
rs_uuid: Uuid, rs_uuid: Uuid,
}, },
V3 {
#[serde(rename = "u")]
refer: Uuid,
#[serde(rename = "p")]
parent: Option<Uuid>,
#[serde(rename = "e")]
state: DbValueSessionStateV1,
#[serde(rename = "i")]
issued_at: String,
#[serde(rename = "r")]
rs_uuid: Uuid,
},
} }
// Internal representation of an image // Internal representation of an image

View file

@ -720,6 +720,108 @@ lazy_static! {
}; };
} }
lazy_static! {
pub static ref IDM_ACP_OAUTH2_MANAGE_DL5: BuiltinAcp = BuiltinAcp {
classes: vec![
EntryClass::Object,
EntryClass::AccessControlProfile,
EntryClass::AccessControlCreate,
EntryClass::AccessControlDelete,
EntryClass::AccessControlModify,
EntryClass::AccessControlSearch
],
name: "idm_acp_hp_oauth2_manage_priv",
uuid: UUID_IDM_ACP_OAUTH2_MANAGE_V1,
description: "Builtin IDM Control for managing oauth2 resource server integrations.",
receiver: BuiltinAcpReceiver::Group(vec![UUID_IDM_OAUTH2_ADMINS]),
target: BuiltinAcpTarget::Filter(ProtoFilter::And(vec![
match_class_filter!(EntryClass::OAuth2ResourceServer),
FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone(),
])),
search_attrs: vec![
Attribute::Class,
Attribute::Description,
Attribute::DisplayName,
Attribute::Name,
Attribute::Spn,
Attribute::OAuth2Session,
Attribute::OAuth2RsOrigin,
Attribute::OAuth2RsOriginLanding,
Attribute::OAuth2RsScopeMap,
Attribute::OAuth2RsSupScopeMap,
Attribute::OAuth2RsBasicSecret,
Attribute::OAuth2RsTokenKey,
Attribute::Es256PrivateKeyDer,
Attribute::OAuth2AllowInsecureClientDisablePkce,
Attribute::Rs256PrivateKeyDer,
Attribute::OAuth2JwtLegacyCryptoEnable,
Attribute::OAuth2PreferShortUsername,
Attribute::OAuth2AllowLocalhostRedirect,
Attribute::OAuth2RsClaimMap,
Attribute::Image,
],
modify_removed_attrs: vec![
Attribute::Description,
Attribute::DisplayName,
Attribute::Name,
Attribute::OAuth2Session,
Attribute::OAuth2RsOrigin,
Attribute::OAuth2RsOriginLanding,
Attribute::OAuth2RsScopeMap,
Attribute::OAuth2RsSupScopeMap,
Attribute::OAuth2RsBasicSecret,
Attribute::OAuth2RsTokenKey,
Attribute::Es256PrivateKeyDer,
Attribute::OAuth2AllowInsecureClientDisablePkce,
Attribute::Rs256PrivateKeyDer,
Attribute::OAuth2JwtLegacyCryptoEnable,
Attribute::OAuth2PreferShortUsername,
Attribute::OAuth2AllowLocalhostRedirect,
Attribute::OAuth2RsClaimMap,
Attribute::Image,
],
modify_present_attrs: vec![
Attribute::Description,
Attribute::DisplayName,
Attribute::Name,
Attribute::OAuth2RsOrigin,
Attribute::OAuth2RsOriginLanding,
Attribute::OAuth2RsSupScopeMap,
Attribute::OAuth2RsScopeMap,
Attribute::OAuth2AllowInsecureClientDisablePkce,
Attribute::OAuth2JwtLegacyCryptoEnable,
Attribute::OAuth2PreferShortUsername,
Attribute::OAuth2AllowLocalhostRedirect,
Attribute::OAuth2RsClaimMap,
Attribute::Image,
],
create_attrs: vec![
Attribute::Class,
Attribute::Description,
Attribute::Name,
Attribute::OAuth2RsName,
Attribute::OAuth2RsOrigin,
Attribute::OAuth2RsOriginLanding,
Attribute::OAuth2RsSupScopeMap,
Attribute::OAuth2RsScopeMap,
Attribute::OAuth2AllowInsecureClientDisablePkce,
Attribute::OAuth2JwtLegacyCryptoEnable,
Attribute::OAuth2PreferShortUsername,
Attribute::OAuth2AllowLocalhostRedirect,
Attribute::OAuth2RsClaimMap,
Attribute::Image,
],
create_classes: vec![
EntryClass::Object,
EntryClass::Account,
EntryClass::OAuth2ResourceServer,
EntryClass::OAuth2ResourceServerBasic,
EntryClass::OAuth2ResourceServerPublic,
],
..Default::default()
};
}
lazy_static! { lazy_static! {
pub static ref IDM_ACP_DOMAIN_ADMIN_V1: BuiltinAcp = BuiltinAcp { pub static ref IDM_ACP_DOMAIN_ADMIN_V1: BuiltinAcp = BuiltinAcp {
classes: vec![ classes: vec![

View file

@ -141,6 +141,7 @@ pub enum Attribute {
PrivateCookieKey, PrivateCookieKey,
PrivilegeExpiry, PrivilegeExpiry,
RadiusSecret, RadiusSecret,
RecycledDirectMemberOf,
Replicated, Replicated,
Rs256PrivateKeyDer, Rs256PrivateKeyDer,
Scope, Scope,
@ -329,6 +330,7 @@ impl TryFrom<String> for Attribute {
ATTR_PRIVATE_COOKIE_KEY => Attribute::PrivateCookieKey, ATTR_PRIVATE_COOKIE_KEY => Attribute::PrivateCookieKey,
ATTR_PRIVILEGE_EXPIRY => Attribute::PrivilegeExpiry, ATTR_PRIVILEGE_EXPIRY => Attribute::PrivilegeExpiry,
ATTR_RADIUS_SECRET => Attribute::RadiusSecret, ATTR_RADIUS_SECRET => Attribute::RadiusSecret,
ATTR_RECYCLEDDIRECTMEMBEROF => Attribute::RecycledDirectMemberOf,
ATTR_REPLICATED => Attribute::Replicated, ATTR_REPLICATED => Attribute::Replicated,
ATTR_RS256_PRIVATE_KEY_DER => Attribute::Rs256PrivateKeyDer, ATTR_RS256_PRIVATE_KEY_DER => Attribute::Rs256PrivateKeyDer,
ATTR_SCOPE => Attribute::Scope, ATTR_SCOPE => Attribute::Scope,
@ -492,6 +494,7 @@ impl From<Attribute> for &'static str {
Attribute::PrivateCookieKey => ATTR_PRIVATE_COOKIE_KEY, Attribute::PrivateCookieKey => ATTR_PRIVATE_COOKIE_KEY,
Attribute::PrivilegeExpiry => ATTR_PRIVILEGE_EXPIRY, Attribute::PrivilegeExpiry => ATTR_PRIVILEGE_EXPIRY,
Attribute::RadiusSecret => ATTR_RADIUS_SECRET, Attribute::RadiusSecret => ATTR_RADIUS_SECRET,
Attribute::RecycledDirectMemberOf => ATTR_RECYCLEDDIRECTMEMBEROF,
Attribute::Replicated => ATTR_REPLICATED, Attribute::Replicated => ATTR_REPLICATED,
Attribute::Rs256PrivateKeyDer => ATTR_RS256_PRIVATE_KEY_DER, Attribute::Rs256PrivateKeyDer => ATTR_RS256_PRIVATE_KEY_DER,
Attribute::Scope => ATTR_SCOPE, Attribute::Scope => ATTR_SCOPE,
@ -826,7 +829,6 @@ impl From<BuiltinAccount> for EntryInitNew {
} }
lazy_static! { lazy_static! {
/// Builtin System Admin account. /// Builtin System Admin account.
pub static ref BUILTIN_ACCOUNT_ADMIN: BuiltinAccount = BuiltinAccount { pub static ref BUILTIN_ACCOUNT_ADMIN: BuiltinAccount = BuiltinAccount {
account_type: AccountType::ServiceAccount, account_type: AccountType::ServiceAccount,
@ -863,12 +865,18 @@ pub const UUID_TESTPERSON_2: Uuid = uuid!("538faac7-4d29-473b-a59d-23023ac19955"
lazy_static! { lazy_static! {
pub static ref E_TESTPERSON_1: EntryInitNew = entry_init!( pub static ref E_TESTPERSON_1: EntryInitNew = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
(Attribute::DisplayName, Value::new_utf8s("Test Person 1")),
(Attribute::Uuid, Value::Uuid(UUID_TESTPERSON_1)) (Attribute::Uuid, Value::Uuid(UUID_TESTPERSON_1))
); );
pub static ref E_TESTPERSON_2: EntryInitNew = entry_init!( pub static ref E_TESTPERSON_2: EntryInitNew = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson2")), (Attribute::Name, Value::new_iname("testperson2")),
(Attribute::DisplayName, Value::new_utf8s("Test Person 2")),
(Attribute::Uuid, Value::Uuid(UUID_TESTPERSON_2)) (Attribute::Uuid, Value::Uuid(UUID_TESTPERSON_2))
); );
} }

View file

@ -43,16 +43,18 @@ pub const SYSTEM_INDEX_VERSION: i64 = 30;
*/ */
pub type DomainVersion = u32; pub type DomainVersion = u32;
pub const DOMAIN_LEVEL_0: DomainVersion = 0;
pub const DOMAIN_LEVEL_1: DomainVersion = 1; pub const DOMAIN_LEVEL_1: DomainVersion = 1;
pub const DOMAIN_LEVEL_2: DomainVersion = 2; pub const DOMAIN_LEVEL_2: DomainVersion = 2;
pub const DOMAIN_LEVEL_3: DomainVersion = 3; pub const DOMAIN_LEVEL_3: DomainVersion = 3;
pub const DOMAIN_LEVEL_4: DomainVersion = 4; pub const DOMAIN_LEVEL_4: DomainVersion = 4;
pub const DOMAIN_LEVEL_5: DomainVersion = 5;
// The minimum supported domain functional level // The minimum supported domain functional level
pub const DOMAIN_MIN_LEVEL: DomainVersion = DOMAIN_LEVEL_2; pub const DOMAIN_MIN_LEVEL: DomainVersion = DOMAIN_LEVEL_5;
// The target supported domain functional level // The target supported domain functional level
pub const DOMAIN_TGT_LEVEL: DomainVersion = DOMAIN_LEVEL_4; pub const DOMAIN_TGT_LEVEL: DomainVersion = DOMAIN_LEVEL_5;
// The maximum supported domain functional level // The maximum supported domain functional level
pub const DOMAIN_MAX_LEVEL: DomainVersion = DOMAIN_LEVEL_4; pub const DOMAIN_MAX_LEVEL: DomainVersion = DOMAIN_LEVEL_5;
// On test builds, define to 60 seconds // On test builds, define to 60 seconds
#[cfg(test)] #[cfg(test)]
@ -65,15 +67,15 @@ pub const PURGE_FREQUENCY: u64 = 600;
/// In test, we limit the changelog to 10 minutes. /// In test, we limit the changelog to 10 minutes.
pub const CHANGELOG_MAX_AGE: u64 = 600; pub const CHANGELOG_MAX_AGE: u64 = 600;
#[cfg(not(test))] #[cfg(not(test))]
/// A replica may be less than 1 day out of sync and catch up. /// A replica may be up to 7 days out of sync before being denied updates.
pub const CHANGELOG_MAX_AGE: u64 = 86400; pub const CHANGELOG_MAX_AGE: u64 = 7 * 86400;
#[cfg(test)] #[cfg(test)]
/// In test, we limit the recyclebin to 5 minutes. /// In test, we limit the recyclebin to 5 minutes.
pub const RECYCLEBIN_MAX_AGE: u64 = 300; pub const RECYCLEBIN_MAX_AGE: u64 = 300;
#[cfg(not(test))] #[cfg(not(test))]
/// In production we allow 1 week /// In production we allow 1 week
pub const RECYCLEBIN_MAX_AGE: u64 = 604_800; pub const RECYCLEBIN_MAX_AGE: u64 = 7 * 86400;
// 5 minute auth session window. // 5 minute auth session window.
pub const AUTH_SESSION_TIMEOUT: u64 = 300; pub const AUTH_SESSION_TIMEOUT: u64 = 300;

View file

@ -637,6 +637,32 @@ pub static ref SCHEMA_CLASS_PERSON: SchemaClass = SchemaClass {
..Default::default() ..Default::default()
}; };
pub static ref SCHEMA_CLASS_PERSON_DL5: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_PERSON,
name: EntryClass::Person.into(),
description: "Object representation of a person".to_string(),
sync_allowed: true,
systemmay: vec![
Attribute::PrimaryCredential.into(),
Attribute::PassKeys.into(),
Attribute::AttestedPasskeys.into(),
Attribute::CredentialUpdateIntentToken.into(),
Attribute::SshPublicKey.into(),
Attribute::RadiusSecret.into(),
Attribute::OAuth2ConsentScopeMap.into(),
Attribute::UserAuthTokenSession.into(),
Attribute::OAuth2Session.into(),
Attribute::Mail.into(),
Attribute::LegalName.into(),
],
systemmust: vec![
Attribute::IdVerificationEcKey.into()
],
systemexcludes: vec![EntryClass::ServiceAccount.into()],
..Default::default()
};
pub static ref SCHEMA_CLASS_ORGPERSON: SchemaClass = SchemaClass { pub static ref SCHEMA_CLASS_ORGPERSON: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_ORGPERSON, uuid: UUID_SCHEMA_CLASS_ORGPERSON,
name: EntryClass::OrgPerson.into(), name: EntryClass::OrgPerson.into(),
@ -725,7 +751,32 @@ pub static ref SCHEMA_CLASS_ACCOUNT: SchemaClass = SchemaClass {
], ],
systemsupplements: vec![ systemsupplements: vec![
EntryClass::Person.into(), EntryClass::Person.into(),
EntryClass::ServiceAccount.into()], EntryClass::ServiceAccount.into(),
],
..Default::default()
};
pub static ref SCHEMA_CLASS_ACCOUNT_DL5: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_ACCOUNT,
name: EntryClass::Account.into(),
description: "Object representation of an account".to_string(),
sync_allowed: true,
systemmay: vec![
Attribute::AccountExpire.into(),
Attribute::AccountValidFrom.into(),
Attribute::NameHistory.into(),
],
systemmust: vec![
Attribute::DisplayName.into(),
Attribute::Name.into(),
Attribute::Spn.into()
],
systemsupplements: vec![
EntryClass::Person.into(),
EntryClass::ServiceAccount.into(),
EntryClass::OAuth2ResourceServer.into(),
],
..Default::default() ..Default::default()
}; };
@ -745,6 +796,27 @@ pub static ref SCHEMA_CLASS_SERVICE_ACCOUNT: SchemaClass = SchemaClass {
..Default::default() ..Default::default()
}; };
pub static ref SCHEMA_CLASS_SERVICE_ACCOUNT_DL5: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_SERVICE_ACCOUNT,
name: EntryClass::ServiceAccount.into(),
description: "Object representation of service account".to_string(),
sync_allowed: true,
systemmay: vec![
Attribute::SshPublicKey.into(),
Attribute::UserAuthTokenSession.into(),
Attribute::OAuth2Session.into(),
Attribute::Description.into(),
Attribute::Mail.into(),
Attribute::PrimaryCredential.into(),
Attribute::JwsEs256PrivateKey.into(),
Attribute::ApiTokenSession.into(),
],
systemexcludes: vec![EntryClass::Person.into()],
..Default::default()
};
pub static ref SCHEMA_CLASS_SYNC_ACCOUNT: SchemaClass = SchemaClass { pub static ref SCHEMA_CLASS_SYNC_ACCOUNT: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_SYNC_ACCOUNT, uuid: UUID_SCHEMA_CLASS_SYNC_ACCOUNT,
name: EntryClass::SyncAccount.into(), name: EntryClass::SyncAccount.into(),
@ -877,6 +949,31 @@ pub static ref SCHEMA_CLASS_OAUTH2_RS_DL4: SchemaClass = SchemaClass {
..Default::default() ..Default::default()
}; };
pub static ref SCHEMA_CLASS_OAUTH2_RS_DL5: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS,
name: EntryClass::OAuth2ResourceServer.into(),
description: "The class representing a configured Oauth2 Resource Server".to_string(),
systemmay: vec![
Attribute::Description.into(),
Attribute::OAuth2RsScopeMap.into(),
Attribute::OAuth2RsSupScopeMap.into(),
Attribute::Rs256PrivateKeyDer.into(),
Attribute::OAuth2JwtLegacyCryptoEnable.into(),
Attribute::OAuth2PreferShortUsername.into(),
Attribute::OAuth2RsOriginLanding.into(),
Attribute::Image.into(),
Attribute::OAuth2RsClaimMap.into(),
Attribute::OAuth2Session.into(),
],
systemmust: vec![
Attribute::OAuth2RsOrigin.into(),
Attribute::OAuth2RsTokenKey.into(),
Attribute::Es256PrivateKeyDer.into(),
],
..Default::default()
};
pub static ref SCHEMA_CLASS_OAUTH2_RS_BASIC: SchemaClass = SchemaClass { pub static ref SCHEMA_CLASS_OAUTH2_RS_BASIC: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS_BASIC, uuid: UUID_SCHEMA_CLASS_OAUTH2_RS_BASIC,
name: EntryClass::OAuth2ResourceServerBasic.into(), name: EntryClass::OAuth2ResourceServerBasic.into(),
@ -888,6 +985,19 @@ pub static ref SCHEMA_CLASS_OAUTH2_RS_BASIC: SchemaClass = SchemaClass {
..Default::default() ..Default::default()
}; };
pub static ref SCHEMA_CLASS_OAUTH2_RS_BASIC_DL5: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS_BASIC,
name: EntryClass::OAuth2ResourceServerBasic.into(),
description: "The class representing a configured Oauth2 Resource Server authenticated with http basic authentication".to_string(),
systemmay: vec![
Attribute::OAuth2AllowInsecureClientDisablePkce.into(),
],
systemmust: vec![ Attribute::OAuth2RsBasicSecret.into()],
systemexcludes: vec![ EntryClass::OAuth2ResourceServerPublic.into()],
..Default::default()
};
pub static ref SCHEMA_CLASS_OAUTH2_RS_PUBLIC: SchemaClass = SchemaClass { pub static ref SCHEMA_CLASS_OAUTH2_RS_PUBLIC: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS_PUBLIC, uuid: UUID_SCHEMA_CLASS_OAUTH2_RS_PUBLIC,
name: EntryClass::OAuth2ResourceServerPublic.into(), name: EntryClass::OAuth2ResourceServerPublic.into(),

View file

@ -274,6 +274,8 @@ pub const UUID_SCHEMA_ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000158"); uuid!("00000000-0000-0000-0000-ffff00000158");
pub const UUID_SCHEMA_ATTR_OAUTH2_RS_CLAIM_MAP: Uuid = pub const UUID_SCHEMA_ATTR_OAUTH2_RS_CLAIM_MAP: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000159"); uuid!("00000000-0000-0000-0000-ffff00000159");
pub const UUID_SCHEMA_ATTR_RECYCLEDDIRECTMEMBEROF: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000160");
// System and domain infos // System and domain infos
// I'd like to strongly criticise william of the past for making poor choices about these allocations. // I'd like to strongly criticise william of the past for making poor choices about these allocations.

View file

@ -1202,6 +1202,7 @@ impl Entry<EntryInvalid, EntryCommitted> {
self.remove_ava(Attribute::Class, &EntryClass::Recycled.into()); self.remove_ava(Attribute::Class, &EntryClass::Recycled.into());
self.remove_ava(Attribute::Class, &EntryClass::Conflict.into()); self.remove_ava(Attribute::Class, &EntryClass::Conflict.into());
self.purge_ava(Attribute::SourceUuid); self.purge_ava(Attribute::SourceUuid);
self.purge_ava(Attribute::RecycledDirectMemberOf);
// Change state repl doesn't need this flag // Change state repl doesn't need this flag
// self.valid.ecstate.revive(&self.valid.cid); // self.valid.ecstate.revive(&self.valid.cid);
@ -2312,6 +2313,25 @@ where
&self.valid.ecstate &self.valid.ecstate
} }
/// Determine if any attribute of this entry changed excluding the attribute named.
/// This allows for detection of entry changes unless the change was to a specific
/// attribute.
pub(crate) fn entry_changed_excluding_attribute(&self, attr: Attribute, cid: &Cid) -> bool {
use crate::repl::entry::State;
match self.get_changestate().current() {
State::Live { at: _, changes } => {
changes.iter().any(|(change_attr, change_id)| {
change_id >= cid &&
change_attr != attr.as_ref() &&
// This always changes, and could throw off other detections.
change_attr != Attribute::LastModifiedCid.as_ref()
})
}
State::Tombstone { at } => at == cid,
}
}
/// ⚠️ - Invalidate an entry by resetting it's change state to time-zero. This entry /// ⚠️ - Invalidate an entry by resetting it's change state to time-zero. This entry
/// can never be replicated after this. /// can never be replicated after this.
/// This is a TEST ONLY method and will never be exposed in production. /// This is a TEST ONLY method and will never be exposed in production.

View file

@ -2008,8 +2008,8 @@ mod tests {
let e1 = entry_init!( let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::Account.to_value()), (Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
( (
Attribute::Uuid, Attribute::Uuid,
@ -2021,7 +2021,8 @@ mod tests {
let e2 = entry_init!( let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Person.to_value().clone()), (Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson2")), (Attribute::Name, Value::new_iname("testperson2")),
( (
Attribute::Uuid, Attribute::Uuid,
@ -2034,7 +2035,8 @@ mod tests {
// We need to add these and then push through the state machine. // We need to add these and then push through the state machine.
let e_ts = entry_init!( let e_ts = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Person.to_value().clone()), (Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson3")), (Attribute::Name, Value::new_iname("testperson3")),
( (
Attribute::Uuid, Attribute::Uuid,
@ -2082,7 +2084,7 @@ mod tests {
let t_uuid = vs_refer![uuid!("a67c0c71-0b35-4218-a6b0-22d23d131d27")] as _; let t_uuid = vs_refer![uuid!("a67c0c71-0b35-4218-a6b0-22d23d131d27")] as _;
let r_uuid = server_txn.resolve_valueset(&t_uuid); let r_uuid = server_txn.resolve_valueset(&t_uuid);
debug!("{:?}", r_uuid); debug!("{:?}", r_uuid);
assert!(r_uuid == Ok(vec!["testperson2".to_string()])); assert!(r_uuid == Ok(vec!["testperson2@example.com".to_string()]));
// Resolve UUID non-exist // Resolve UUID non-exist
let t_uuid_non = vs_refer![uuid!("b83e98f0-3d2e-41d2-9796-d8d993289c86")] as _; let t_uuid_non = vs_refer![uuid!("b83e98f0-3d2e-41d2-9796-d8d993289c86")] as _;

View file

@ -49,7 +49,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
.cloned()?; .cloned()?;
let name = entry let name = entry
.get_ava_single_iname(Attribute::OAuth2RsName) .get_ava_single_iname(Attribute::Name)
.map(str::to_string)?; .map(str::to_string)?;
Some(AppLink::Oauth2 { Some(AppLink::Oauth2 {
@ -84,6 +84,7 @@ mod tests {
let e_rs: Entry<EntryInit, EntryNew> = entry_init!( let e_rs: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
( (
Attribute::Class, Attribute::Class,
EntryClass::OAuth2ResourceServer.to_value() EntryClass::OAuth2ResourceServer.to_value()
@ -92,10 +93,7 @@ mod tests {
Attribute::Class, Attribute::Class,
EntryClass::OAuth2ResourceServerBasic.to_value() EntryClass::OAuth2ResourceServerBasic.to_value()
), ),
( (Attribute::Name, Value::new_iname("test_resource_server")),
Attribute::OAuth2RsName,
Value::new_iname("test_resource_server")
),
( (
Attribute::DisplayName, Attribute::DisplayName,
Value::new_utf8s("test_resource_server") Value::new_utf8s("test_resource_server")

View file

@ -154,6 +154,14 @@ pub(crate) enum Oauth2TokenType {
// We stash some details here for oidc. // We stash some details here for oidc.
nonce: Option<String>, nonce: Option<String>,
}, },
ClientAccess {
scopes: BTreeSet<String>,
session_id: Uuid,
uuid: Uuid,
expiry: time::OffsetDateTime,
iat: i64,
nbf: i64,
},
} }
impl fmt::Display for Oauth2TokenType { impl fmt::Display for Oauth2TokenType {
@ -165,6 +173,9 @@ impl fmt::Display for Oauth2TokenType {
Oauth2TokenType::Refresh { session_id, .. } => { Oauth2TokenType::Refresh { session_id, .. } => {
write!(f, "refresh_token ({session_id}) ") write!(f, "refresh_token ({session_id}) ")
} }
Oauth2TokenType::ClientAccess { session_id, .. } => {
write!(f, "client_access_token ({session_id})")
}
} }
} }
} }
@ -275,6 +286,8 @@ pub struct Oauth2RS {
claim_map: BTreeMap<Uuid, Vec<(String, ClaimValue)>>, claim_map: BTreeMap<Uuid, Vec<(String, ClaimValue)>>,
scope_maps: BTreeMap<Uuid, BTreeSet<String>>, scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
sup_scope_maps: BTreeMap<Uuid, BTreeSet<String>>, sup_scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
client_scopes: BTreeSet<String>,
client_sup_scopes: BTreeSet<String>,
// Our internal exchange encryption material for this rs. // Our internal exchange encryption material for this rs.
token_fernet: Fernet, token_fernet: Fernet,
jws_signer: Oauth2JwsSigner, jws_signer: Oauth2JwsSigner,
@ -409,7 +422,7 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
// Now we know we can load the shared attrs. // Now we know we can load the shared attrs.
let name = ent let name = ent
.get_ava_single_iname(Attribute::OAuth2RsName) .get_ava_single_iname(Attribute::Name)
.map(str::to_string) .map(str::to_string)
.ok_or(OperationError::InvalidValueState)?; .ok_or(OperationError::InvalidValueState)?;
@ -449,6 +462,41 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
.cloned() .cloned()
.unwrap_or_default(); .unwrap_or_default();
// From our scope maps we can now determine what scopes would be granted to our
// client during a client credentials authentication.
let (client_scopes, client_sup_scopes) = if let Some(client_member_of) = ent.get_ava_refer(Attribute::MemberOf) {
let client_scopes =
scope_maps
.iter()
.filter_map(|(u, m)| {
if client_member_of.contains(u) {
Some(m.iter())
} else {
None
}
})
.flatten()
.cloned()
.collect::<BTreeSet<_>>();
let client_sup_scopes = sup_scope_maps
.iter()
.filter_map(|(u, m)| {
if client_member_of.contains(u) {
Some(m.iter())
} else {
None
}
})
.flatten()
.cloned()
.collect::<BTreeSet<_>>();
(client_scopes, client_sup_scopes)
} else {
(BTreeSet::default(), BTreeSet::default())
};
let e_claim_maps = ent let e_claim_maps = ent
.get_ava_set(Attribute::OAuth2RsClaimMap) .get_ava_set(Attribute::OAuth2RsClaimMap)
.and_then(|vs| vs.as_oauthclaim_map()); .and_then(|vs| vs.as_oauthclaim_map());
@ -573,6 +621,8 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
origin_https, origin_https,
scope_maps, scope_maps,
sup_scope_maps, sup_scope_maps,
client_scopes,
client_sup_scopes,
claim_map, claim_map,
token_fernet, token_fernet,
jws_signer, jws_signer,
@ -641,7 +691,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
.token_fernet .token_fernet
.decrypt(&revoke_req.token) .decrypt(&revoke_req.token)
.map_err(|_| { .map_err(|_| {
admin_error!("Failed to decrypt token introspection request"); admin_error!("Failed to decrypt token revoke request");
Oauth2Error::InvalidRequest Oauth2Error::InvalidRequest
}) })
.and_then(|data| { .and_then(|data| {
@ -660,6 +710,12 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
uuid, uuid,
.. ..
} }
| Oauth2TokenType::ClientAccess {
session_id,
expiry,
uuid,
..
}
| Oauth2TokenType::Refresh { | Oauth2TokenType::Refresh {
session_id, session_id,
expiry, expiry,
@ -738,11 +794,13 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
}; };
// check the secret. // check the secret.
match &o2rs.type_ { let client_authentication_valid = match &o2rs.type_ {
OauthRSType::Basic { authz_secret, .. } => { OauthRSType::Basic { authz_secret, .. } => {
match secret { match secret {
Some(secret) => { Some(secret) => {
if authz_secret != &secret { if authz_secret == &secret {
true
} else {
security_info!("Invalid OAuth2 client_id secret"); security_info!("Invalid OAuth2 client_id secret");
return Err(Oauth2Error::AuthenticationRequired); return Err(Oauth2Error::AuthenticationRequired);
} }
@ -757,14 +815,10 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
} }
} }
// Relies on the token to be valid - no further action needed. // Relies on the token to be valid - no further action needed.
OauthRSType::Public { .. } => {} OauthRSType::Public { .. } => false,
}; };
// We are authenticated! Yay! Now we can actually check things ... // We are authenticated! Yay! Now we can actually check things ...
// TODO: add refresh token grant type.
// If it's a refresh token grant, are the consent permissions the same?
match &token_req.grant_type { match &token_req.grant_type {
GrantTypeReq::AuthorizationCode { GrantTypeReq::AuthorizationCode {
code, code,
@ -777,6 +831,16 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
code_verifier.as_deref(), code_verifier.as_deref(),
ct, ct,
), ),
GrantTypeReq::ClientCredentials { scope } => {
if client_authentication_valid {
self.check_oauth2_token_client_credentials(o2rs, scope.as_ref(), ct)
} else {
security_info!(
"Unable to proceed with client credentials grant unless client authentication is provided and valid"
);
Err(Oauth2Error::AuthenticationRequired)
}
}
GrantTypeReq::RefreshToken { GrantTypeReq::RefreshToken {
refresh_token, refresh_token,
scope, scope,
@ -1011,8 +1075,8 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
})?; })?;
match token { match token {
Oauth2TokenType::Access { .. } => { Oauth2TokenType::Access { .. } | Oauth2TokenType::ClientAccess { .. } => {
admin_error!("attempt to refresh with access token"); admin_error!("attempt to refresh with an access token");
Err(Oauth2Error::InvalidToken) Err(Oauth2Error::InvalidToken)
} }
Oauth2TokenType::Refresh { Oauth2TokenType::Refresh {
@ -1035,7 +1099,13 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
// Check the session is still valid. This call checks the parent session // Check the session is still valid. This call checks the parent session
// and the OAuth2 session. // and the OAuth2 session.
let valid = self let valid = self
.check_oauth2_account_uuid_valid(uuid, session_id, parent_session_id, iat, ct) .check_oauth2_account_uuid_valid(
uuid,
session_id,
Some(parent_session_id),
iat,
ct,
)
.map_err(|_| admin_error!("Account is not valid")); .map_err(|_| admin_error!("Account is not valid"));
let Ok(Some(entry)) = valid else { let Ok(Some(entry)) = valid else {
@ -1117,6 +1187,111 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
} }
} }
#[instrument(level = "debug", skip_all)]
fn check_oauth2_token_client_credentials(
&mut self,
o2rs: &Oauth2RS,
req_scopes: Option<&BTreeSet<String>>,
ct: Duration,
) -> Result<AccessTokenResponse, Oauth2Error> {
let req_scopes = req_scopes.cloned().unwrap_or_default();
// Validate all request scopes have valid syntax.
validate_scopes(&req_scopes)?;
// Of these scopes, which do we have available?
let avail_scopes: Vec<String> = req_scopes
.intersection(&o2rs.client_scopes)
.map(|s| s.to_string())
.collect();
if avail_scopes.len() != req_scopes.len() {
admin_warn!(
ident = %o2rs.name,
requested_scopes = ?req_scopes,
available_scopes = ?o2rs.client_scopes,
"Client does not have access to the requested scopes"
);
return Err(Oauth2Error::AccessDenied);
}
// == ready to build the access token ==
let granted_scopes = avail_scopes
.into_iter()
.chain(o2rs.client_sup_scopes.iter().cloned())
.collect::<BTreeSet<_>>();
let odt_ct = OffsetDateTime::UNIX_EPOCH + ct;
let iat = ct.as_secs() as i64;
let expiry = odt_ct + Duration::from_secs(OAUTH2_ACCESS_TOKEN_EXPIRY as u64);
let expires_in = OAUTH2_ACCESS_TOKEN_EXPIRY;
let session_id = Uuid::new_v4();
let scope = if granted_scopes.is_empty() {
None
} else {
Some(str_join(&granted_scopes))
};
let uuid = o2rs.uuid;
let access_token_raw = Oauth2TokenType::ClientAccess {
scopes: granted_scopes,
session_id,
uuid,
expiry,
iat,
nbf: iat,
};
let access_token_data = serde_json::to_vec(&access_token_raw).map_err(|e| {
admin_error!(err = ?e, "Unable to encode token data");
Oauth2Error::ServerError(OperationError::SerdeJsonError)
})?;
let access_token = o2rs
.token_fernet
.encrypt_at_time(&access_token_data, ct.as_secs());
// Write the session to the db
let session = Value::Oauth2Session(
session_id,
Oauth2Session {
parent: None,
state: SessionState::ExpiresAt(expiry),
issued_at: odt_ct,
rs_uuid: o2rs.uuid,
},
);
// We need to create this session on the o2rs
let modlist = ModifyList::new_list(vec![Modify::Present(
Attribute::OAuth2Session.into(),
session,
)]);
self.qs_write
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(uuid))),
&modlist,
)
.map_err(|e| {
admin_error!("Failed to persist OAuth2 session record {:?}", e);
Oauth2Error::ServerError(e)
})?;
Ok(AccessTokenResponse {
access_token,
token_type: "Bearer".to_string(),
expires_in,
refresh_token: None,
scope,
id_token: None,
})
}
fn generate_access_token_response( fn generate_access_token_response(
&mut self, &mut self,
o2rs: &Oauth2RS, o2rs: &Oauth2RS,
@ -1271,7 +1446,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
let session = Value::Oauth2Session( let session = Value::Oauth2Session(
session_id, session_id,
Oauth2Session { Oauth2Session {
parent: parent_session_id, parent: Some(parent_session_id),
state: SessionState::ExpiresAt(refresh_expiry), state: SessionState::ExpiresAt(refresh_expiry),
issued_at: odt_ct, issued_at: odt_ct,
rs_uuid: o2rs.uuid, rs_uuid: o2rs.uuid,
@ -1338,7 +1513,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
o2rs.token_fernet o2rs.token_fernet
.decrypt(token) .decrypt(token)
.map_err(|_| { .map_err(|_| {
admin_error!("Failed to decrypt token introspection request"); admin_error!("Failed to decrypt token reflection request");
OperationError::CryptographyError OperationError::CryptographyError
}) })
.and_then(|data| { .and_then(|data| {
@ -1495,25 +1670,8 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
return Err(Oauth2Error::InvalidRequest); return Err(Oauth2Error::InvalidRequest);
} }
let failed_scopes = req_scopes // Validate all request scopes have valid syntax.
.iter() validate_scopes(&req_scopes)?;
.filter(|&s| !OAUTHSCOPE_RE.is_match(s))
.cloned()
.collect::<Vec<String>>();
if !failed_scopes.is_empty() {
let requested_scopes_string = req_scopes
.iter()
.cloned()
.collect::<Vec<String>>()
.join(",");
admin_error!(
"Invalid OAuth2 request - requested scopes ({}) but ({}) failed to pass validation rules - all must match the regex {}",
requested_scopes_string,
failed_scopes.join(","),
OAUTHSCOPE_RE.as_str()
);
return Err(Oauth2Error::InvalidScope);
}
let uat_scopes: BTreeSet<String> = o2rs let uat_scopes: BTreeSet<String> = o2rs
.scope_maps .scope_maps
@ -1758,6 +1916,8 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
// We are authenticated! Yay! Now we can actually check things ... // We are authenticated! Yay! Now we can actually check things ...
let prefer_short_username = o2rs.prefer_short_username;
let token: Oauth2TokenType = o2rs let token: Oauth2TokenType = o2rs
.token_fernet .token_fernet
.decrypt(&intr_req.token) .decrypt(&intr_req.token)
@ -1793,13 +1953,19 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
// Is the user expired, or the OAuth2 session invalid? // Is the user expired, or the OAuth2 session invalid?
let valid = self let valid = self
.check_oauth2_account_uuid_valid(uuid, session_id, parent_session_id, iat, ct) .check_oauth2_account_uuid_valid(
uuid,
session_id,
Some(parent_session_id),
iat,
ct,
)
.map_err(|_| admin_error!("Account is not valid")); .map_err(|_| admin_error!("Account is not valid"));
let Ok(Some(entry)) = valid else { let Ok(Some(entry)) = valid else {
security_info!( security_info!(
?uuid, ?uuid,
"access token has no account not valid, returning inactive" "access token account is not valid, returning inactive"
); );
return Ok(AccessTokenIntrospectResponse::inactive()); return Ok(AccessTokenIntrospectResponse::inactive());
}; };
@ -1819,12 +1985,79 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
let exp = expiry.unix_timestamp(); let exp = expiry.unix_timestamp();
let preferred_username = if prefer_short_username {
Some(account.name.clone())
} else {
Some(account.spn.clone())
};
let token_type = Some("access_token".to_string()); let token_type = Some("access_token".to_string());
Ok(AccessTokenIntrospectResponse { Ok(AccessTokenIntrospectResponse {
active: true, active: true,
scope, scope,
client_id: Some(client_id.clone()), client_id: Some(client_id.clone()),
username: Some(account.spn), username: preferred_username,
token_type,
iat: Some(iat),
exp: Some(exp),
nbf: Some(nbf),
sub: Some(uuid.to_string()),
aud: Some(client_id),
iss: None,
jti: None,
})
}
Oauth2TokenType::ClientAccess {
scopes,
session_id,
uuid,
expiry,
iat,
nbf,
} => {
// Has this token expired?
let odt_ct = OffsetDateTime::UNIX_EPOCH + ct;
if expiry <= odt_ct {
security_info!(?uuid, "access token has expired, returning inactive");
return Ok(AccessTokenIntrospectResponse::inactive());
}
// We can't do the same validity check for the client as we do with an account
let valid = self
.check_oauth2_account_uuid_valid(uuid, session_id, None, iat, ct)
.map_err(|_| admin_error!("Account is not valid"));
let Ok(Some(entry)) = valid else {
security_info!(
?uuid,
"access token account is not valid, returning inactive"
);
return Ok(AccessTokenIntrospectResponse::inactive());
};
let scope = if scopes.is_empty() {
None
} else {
Some(str_join(&scopes))
};
let exp = expiry.unix_timestamp();
let token_type = Some("access_token".to_string());
let username = if prefer_short_username {
entry
.get_ava_single_iname(Attribute::Name)
.map(|s| s.to_string())
} else {
entry.get_ava_single_proto_string(Attribute::Spn)
};
Ok(AccessTokenIntrospectResponse {
active: true,
scope,
client_id: Some(client_id.clone()),
username,
token_type, token_type,
iat: Some(iat), iat: Some(iat),
exp: Some(exp), exp: Some(exp),
@ -1897,7 +2130,13 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
// Is the user expired, or the OAuth2 session invalid? // Is the user expired, or the OAuth2 session invalid?
let valid = self let valid = self
.check_oauth2_account_uuid_valid(uuid, session_id, parent_session_id, iat, ct) .check_oauth2_account_uuid_valid(
uuid,
session_id,
Some(parent_session_id),
iat,
ct,
)
.map_err(|_| admin_error!("Account is not valid")); .map_err(|_| admin_error!("Account is not valid"));
let Ok(Some(entry)) = valid else { let Ok(Some(entry)) = valid else {
@ -1942,6 +2181,10 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
}) })
} }
// https://openid.net/specs/openid-connect-basic-1_0.html#UserInfoErrorResponse // https://openid.net/specs/openid-connect-basic-1_0.html#UserInfoErrorResponse
Oauth2TokenType::ClientAccess { .. } => {
warn!("OpenID userinfo introspection of client access tokens is not currently supported.");
Err(Oauth2Error::InvalidToken)
}
Oauth2TokenType::Refresh { .. } => Err(Oauth2Error::InvalidToken), Oauth2TokenType::Refresh { .. } => Err(Oauth2Error::InvalidToken),
} }
} }
@ -2252,6 +2495,30 @@ fn str_join(set: &BTreeSet<String>) -> String {
buf buf
} }
fn validate_scopes(req_scopes: &BTreeSet<String>) -> Result<(), Oauth2Error> {
let failed_scopes = req_scopes
.iter()
.filter(|&s| !OAUTHSCOPE_RE.is_match(s))
.cloned()
.collect::<Vec<String>>();
if !failed_scopes.is_empty() {
let requested_scopes_string = req_scopes
.iter()
.cloned()
.collect::<Vec<String>>()
.join(",");
admin_error!(
"Invalid OAuth2 request - requested scopes ({}) but ({}) failed to pass validation rules - all must match the regex {}",
requested_scopes_string,
failed_scopes.join(","),
OAUTHSCOPE_RE.as_str()
);
return Err(Oauth2Error::InvalidScope);
}
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use base64::{engine::general_purpose, Engine as _}; use base64::{engine::general_purpose, Engine as _};
@ -2284,6 +2551,8 @@ mod tests {
const UAT_EXPIRE: u64 = 5; const UAT_EXPIRE: u64 = 5;
const TOKEN_EXPIRE: u64 = 900; const TOKEN_EXPIRE: u64 = 900;
const UUID_TESTGROUP: Uuid = uuid!("a3028223-bf20-47d5-8b65-967b5d2bb3eb");
macro_rules! create_code_verifier { macro_rules! create_code_verifier {
($key:expr) => {{ ($key:expr) => {{
let code_verifier = $key.to_string(); let code_verifier = $key.to_string();
@ -2333,10 +2602,19 @@ mod tests {
) -> (String, UserAuthToken, Identity, Uuid) { ) -> (String, UserAuthToken, Identity, Uuid) {
let mut idms_prox_write = idms.proxy_write(ct).await; let mut idms_prox_write = idms.proxy_write(ct).await;
let uuid = Uuid::new_v4(); let rs_uuid = Uuid::new_v4();
let e: Entry<EntryInit, EntryNew> = entry_init!( let entry_group: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup")),
(Attribute::Description, Value::new_utf8s("testgroup")),
(Attribute::Uuid, Value::Uuid(UUID_TESTGROUP)),
(Attribute::Member, Value::Refer(UUID_TESTPERSON_1),)
);
let entry_rs: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
( (
Attribute::Class, Attribute::Class,
EntryClass::OAuth2ResourceServer.to_value() EntryClass::OAuth2ResourceServer.to_value()
@ -2345,11 +2623,8 @@ mod tests {
Attribute::Class, Attribute::Class,
EntryClass::OAuth2ResourceServerBasic.to_value() EntryClass::OAuth2ResourceServerBasic.to_value()
), ),
(Attribute::Uuid, Value::Uuid(uuid)), (Attribute::Uuid, Value::Uuid(rs_uuid)),
( (Attribute::Name, Value::new_iname("test_resource_server")),
Attribute::OAuth2RsName,
Value::new_iname("test_resource_server")
),
( (
Attribute::DisplayName, Attribute::DisplayName,
Value::new_utf8s("test_resource_server") Value::new_utf8s("test_resource_server")
@ -2362,7 +2637,7 @@ mod tests {
( (
Attribute::OAuth2RsScopeMap, Attribute::OAuth2RsScopeMap,
Value::new_oauthscopemap( Value::new_oauthscopemap(
UUID_SYSTEM_ADMINS, UUID_TESTGROUP,
btreeset![OAUTH2_SCOPE_GROUPS.to_string()] btreeset![OAUTH2_SCOPE_GROUPS.to_string()]
) )
.expect("invalid oauthscope") .expect("invalid oauthscope")
@ -2396,12 +2671,12 @@ mod tests {
Value::new_bool(prefer_short_username) Value::new_bool(prefer_short_username)
) )
); );
let ce = CreateEvent::new_internal(vec![e]); let ce = CreateEvent::new_internal(vec![entry_rs, entry_group, E_TESTPERSON_1.clone()]);
assert!(idms_prox_write.qs_write.create(&ce).is_ok()); assert!(idms_prox_write.qs_write.create(&ce).is_ok());
let entry = idms_prox_write let entry = idms_prox_write
.qs_write .qs_write
.internal_search_uuid(uuid) .internal_search_uuid(rs_uuid)
.expect("Failed to retrieve OAuth2 resource entry "); .expect("Failed to retrieve OAuth2 resource entry ");
let secret = entry let secret = entry
.get_ava_single_secret(Attribute::OAuth2RsBasicSecret) .get_ava_single_secret(Attribute::OAuth2RsBasicSecret)
@ -2410,12 +2685,12 @@ mod tests {
// Setup the uat we'll be using - note for these tests they *require* // Setup the uat we'll be using - note for these tests they *require*
// the parent session to be valid and present! // the parent session to be valid and present!
let session_id = uuid::Uuid::new_v4(); let session_id = uuid::Uuid::new_v4();
let account = idms_prox_write let account = idms_prox_write
.target_to_account(UUID_ADMIN) .target_to_account(UUID_TESTPERSON_1)
.expect("account must exist"); .expect("account must exist");
let uat = account let uat = account
.to_userauthtoken( .to_userauthtoken(
session_id, session_id,
@ -2459,7 +2734,7 @@ mod tests {
idms_prox_write idms_prox_write
.qs_write .qs_write
.internal_modify( .internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_ADMIN))), &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
&modlist, &modlist,
) )
.expect("Failed to modify user"); .expect("Failed to modify user");
@ -2470,7 +2745,7 @@ mod tests {
idms_prox_write.commit().expect("failed to commit"); idms_prox_write.commit().expect("failed to commit");
(secret, uat, ident, uuid) (secret, uat, ident, rs_uuid)
} }
async fn setup_oauth2_resource_server_public( async fn setup_oauth2_resource_server_public(
@ -2479,10 +2754,19 @@ mod tests {
) -> (UserAuthToken, Identity, Uuid) { ) -> (UserAuthToken, Identity, Uuid) {
let mut idms_prox_write = idms.proxy_write(ct).await; let mut idms_prox_write = idms.proxy_write(ct).await;
let uuid = Uuid::new_v4(); let rs_uuid = Uuid::new_v4();
let e: Entry<EntryInit, EntryNew> = entry_init!( let entry_group: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup")),
(Attribute::Description, Value::new_utf8s("testgroup")),
(Attribute::Uuid, Value::Uuid(UUID_TESTGROUP)),
(Attribute::Member, Value::Refer(UUID_TESTPERSON_1),)
);
let entry_rs: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
( (
Attribute::Class, Attribute::Class,
EntryClass::OAuth2ResourceServer.to_value() EntryClass::OAuth2ResourceServer.to_value()
@ -2491,11 +2775,8 @@ mod tests {
Attribute::Class, Attribute::Class,
EntryClass::OAuth2ResourceServerPublic.to_value() EntryClass::OAuth2ResourceServerPublic.to_value()
), ),
(Attribute::Uuid, Value::Uuid(uuid)), (Attribute::Uuid, Value::Uuid(rs_uuid)),
( (Attribute::Name, Value::new_iname("test_resource_server")),
Attribute::OAuth2RsName,
Value::new_iname("test_resource_server")
),
( (
Attribute::DisplayName, Attribute::DisplayName,
Value::new_utf8s("test_resource_server") Value::new_utf8s("test_resource_server")
@ -2507,7 +2788,7 @@ mod tests {
// System admins // System admins
( (
Attribute::OAuth2RsScopeMap, Attribute::OAuth2RsScopeMap,
Value::new_oauthscopemap(UUID_SYSTEM_ADMINS, btreeset!["groups".to_string()]) Value::new_oauthscopemap(UUID_TESTGROUP, btreeset!["groups".to_string()])
.expect("invalid oauthscope") .expect("invalid oauthscope")
), ),
( (
@ -2527,7 +2808,7 @@ mod tests {
.expect("invalid oauthscope") .expect("invalid oauthscope")
) )
); );
let ce = CreateEvent::new_internal(vec![e]); let ce = CreateEvent::new_internal(vec![entry_rs, entry_group, E_TESTPERSON_1.clone()]);
assert!(idms_prox_write.qs_write.create(&ce).is_ok()); assert!(idms_prox_write.qs_write.create(&ce).is_ok());
// Setup the uat we'll be using - note for these tests they *require* // Setup the uat we'll be using - note for these tests they *require*
@ -2536,7 +2817,7 @@ mod tests {
let session_id = uuid::Uuid::new_v4(); let session_id = uuid::Uuid::new_v4();
let account = idms_prox_write let account = idms_prox_write
.target_to_account(UUID_ADMIN) .target_to_account(UUID_TESTPERSON_1)
.expect("account must exist"); .expect("account must exist");
let uat = account let uat = account
.to_userauthtoken( .to_userauthtoken(
@ -2581,7 +2862,7 @@ mod tests {
idms_prox_write idms_prox_write
.qs_write .qs_write
.internal_modify( .internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_ADMIN))), &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
&modlist, &modlist,
) )
.expect("Failed to modify user"); .expect("Failed to modify user");
@ -2592,7 +2873,7 @@ mod tests {
idms_prox_write.commit().expect("failed to commit"); idms_prox_write.commit().expect("failed to commit");
(uat, ident, uuid) (uat, ident, rs_uuid)
} }
async fn setup_idm_admin(idms: &IdmServer, ct: Duration) -> (UserAuthToken, Identity) { async fn setup_idm_admin(idms: &IdmServer, ct: Duration) -> (UserAuthToken, Identity) {
@ -3233,7 +3514,7 @@ mod tests {
assert!(intr_response.active); assert!(intr_response.active);
assert!(intr_response.scope.as_deref() == Some("openid supplement")); assert!(intr_response.scope.as_deref() == Some("openid supplement"));
assert!(intr_response.client_id.as_deref() == Some("test_resource_server")); assert!(intr_response.client_id.as_deref() == Some("test_resource_server"));
assert!(intr_response.username.as_deref() == Some("admin@example.com")); assert!(intr_response.username.as_deref() == Some("testperson1@example.com"));
assert!(intr_response.token_type.as_deref() == Some("access_token")); assert!(intr_response.token_type.as_deref() == Some("access_token"));
assert!(intr_response.iat == Some(ct.as_secs() as i64)); assert!(intr_response.iat == Some(ct.as_secs() as i64));
assert!(intr_response.nbf == Some(ct.as_secs() as i64)); assert!(intr_response.nbf == Some(ct.as_secs() as i64));
@ -3245,7 +3526,7 @@ mod tests {
// Expire the account, should cause introspect to return inactive. // Expire the account, should cause introspect to return inactive.
let v_expire = Value::new_datetime_epoch(Duration::from_secs(TEST_CURRENT_TIME - 1)); let v_expire = Value::new_datetime_epoch(Duration::from_secs(TEST_CURRENT_TIME - 1));
let me_inv_m = ModifyEvent::new_internal_invalid( let me_inv_m = ModifyEvent::new_internal_invalid(
filter!(f_eq(Attribute::Name, PartialValue::new_iname("admin"))), filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
ModifyList::new_list(vec![Modify::Present( ModifyList::new_list(vec![Modify::Present(
Attribute::AccountExpire.into(), Attribute::AccountExpire.into(),
v_expire, v_expire,
@ -3486,8 +3767,10 @@ mod tests {
.expect("Failed to access internals of the refresh token"); .expect("Failed to access internals of the refresh token");
let session_id = match reflected_token { let session_id = match reflected_token {
Oauth2TokenType::Refresh { session_id, .. } => session_id,
Oauth2TokenType::Access { session_id, .. } => session_id, Oauth2TokenType::Access { session_id, .. } => session_id,
Oauth2TokenType::Refresh { .. } | Oauth2TokenType::ClientAccess { .. } => {
unreachable!()
}
}; };
assert!(idms_prox_write.commit().is_ok()); assert!(idms_prox_write.commit().is_ok());
@ -3498,7 +3781,7 @@ mod tests {
// Check it is now there // Check it is now there
let entry = idms_prox_write let entry = idms_prox_write
.qs_write .qs_write
.internal_search_uuid(UUID_ADMIN) .internal_search_uuid(UUID_TESTPERSON_1)
.expect("failed"); .expect("failed");
let valid = entry let valid = entry
.get_ava_as_oauth2session_map(Attribute::OAuth2Session) .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
@ -3509,7 +3792,7 @@ mod tests {
// Delete the resource server. // Delete the resource server.
let de = DeleteEvent::new_internal_invalid(filter!(f_eq( let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
Attribute::OAuth2RsName, Attribute::Name,
PartialValue::new_iname("test_resource_server") PartialValue::new_iname("test_resource_server")
))); )));
@ -3520,7 +3803,7 @@ mod tests {
// revoked. // revoked.
let entry = idms_prox_write let entry = idms_prox_write
.qs_write .qs_write
.internal_search_uuid(UUID_ADMIN) .internal_search_uuid(UUID_TESTPERSON_1)
.expect("failed"); .expect("failed");
let revoked = entry let revoked = entry
.get_ava_as_oauth2session_map(Attribute::OAuth2Session) .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
@ -3953,7 +4236,7 @@ mod tests {
== Url::parse("https://idm.example.com/oauth2/openid/test_resource_server") == Url::parse("https://idm.example.com/oauth2/openid/test_resource_server")
.unwrap() .unwrap()
); );
assert!(oidc.sub == OidcSubject::U(UUID_ADMIN)); assert!(oidc.sub == OidcSubject::U(UUID_TESTPERSON_1));
assert!(oidc.aud == "test_resource_server"); assert!(oidc.aud == "test_resource_server");
assert!(oidc.iat == iat); assert!(oidc.iat == iat);
assert!(oidc.nbf == Some(iat)); assert!(oidc.nbf == Some(iat));
@ -3967,8 +4250,8 @@ mod tests {
assert!(oidc.amr.is_none()); assert!(oidc.amr.is_none());
assert!(oidc.azp == Some("test_resource_server".to_string())); assert!(oidc.azp == Some("test_resource_server".to_string()));
assert!(oidc.jti.is_none()); assert!(oidc.jti.is_none());
assert!(oidc.s_claims.name == Some("System Administrator".to_string())); assert!(oidc.s_claims.name == Some("Test Person 1".to_string()));
assert!(oidc.s_claims.preferred_username == Some("admin@example.com".to_string())); assert!(oidc.s_claims.preferred_username == Some("testperson1@example.com".to_string()));
assert!( assert!(
oidc.s_claims.scopes == vec![OAUTH2_SCOPE_OPENID.to_string(), "supplement".to_string()] oidc.s_claims.scopes == vec![OAUTH2_SCOPE_OPENID.to_string(), "supplement".to_string()]
); );
@ -4119,7 +4402,7 @@ mod tests {
.expect("Failed to verify oidc"); .expect("Failed to verify oidc");
// Do we have the short username in the token claims? // Do we have the short username in the token claims?
assert!(oidc.s_claims.preferred_username == Some("admin".to_string())); assert!(oidc.s_claims.preferred_username == Some("testperson1".to_string()));
// Do the id_token details line up to the userinfo? // Do the id_token details line up to the userinfo?
let userinfo = idms_prox_read let userinfo = idms_prox_read
.oauth2_openid_userinfo("test_resource_server", &access_token, ct) .oauth2_openid_userinfo("test_resource_server", &access_token, ct)
@ -4364,7 +4647,7 @@ mod tests {
.verify_exp(iat) .verify_exp(iat)
.expect("Failed to verify oidc"); .expect("Failed to verify oidc");
assert!(oidc.sub == OidcSubject::U(UUID_ADMIN)); assert!(oidc.sub == OidcSubject::U(UUID_TESTPERSON_1));
assert!(idms_prox_write.commit().is_ok()); assert!(idms_prox_write.commit().is_ok());
} }
@ -4439,7 +4722,7 @@ mod tests {
let me_extend_scopes = ModifyEvent::new_internal_invalid( let me_extend_scopes = ModifyEvent::new_internal_invalid(
filter!(f_eq( filter!(f_eq(
Attribute::OAuth2RsName, Attribute::Name,
PartialValue::new_iname("test_resource_server") PartialValue::new_iname("test_resource_server")
)), )),
ModifyList::new_list(vec![Modify::Present( ModifyList::new_list(vec![Modify::Present(
@ -4505,7 +4788,7 @@ mod tests {
let me_extend_scopes = ModifyEvent::new_internal_invalid( let me_extend_scopes = ModifyEvent::new_internal_invalid(
filter!(f_eq( filter!(f_eq(
Attribute::OAuth2RsName, Attribute::Name,
PartialValue::new_iname("test_resource_server") PartialValue::new_iname("test_resource_server")
)), )),
ModifyList::new_list(vec![Modify::Present( ModifyList::new_list(vec![Modify::Present(
@ -4617,7 +4900,7 @@ mod tests {
// Now trigger the delete of the RS // Now trigger the delete of the RS
let de = DeleteEvent::new_internal_invalid(filter!(f_eq( let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
Attribute::OAuth2RsName, Attribute::Name,
PartialValue::new_iname("test_resource_server") PartialValue::new_iname("test_resource_server")
))); )));
@ -4935,7 +5218,7 @@ mod tests {
let refresh_exp = match reflected_token { let refresh_exp = match reflected_token {
Oauth2TokenType::Refresh { expiry, .. } => expiry.unix_timestamp(), Oauth2TokenType::Refresh { expiry, .. } => expiry.unix_timestamp(),
Oauth2TokenType::Access { .. } => unreachable!(), Oauth2TokenType::Access { .. } | Oauth2TokenType::ClientAccess { .. } => unreachable!(),
}; };
let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken { let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
@ -5176,7 +5459,7 @@ mod tests {
let entry = idms_prox_write let entry = idms_prox_write
.qs_write .qs_write
.internal_search_uuid(UUID_ADMIN) .internal_search_uuid(UUID_TESTPERSON_1)
.expect("failed"); .expect("failed");
let valid = entry let valid = entry
.get_ava_as_oauth2session_map(Attribute::OAuth2Session) .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
@ -5295,7 +5578,7 @@ mod tests {
Attribute::OAuth2RsClaimMap.into(), Attribute::OAuth2RsClaimMap.into(),
Value::OauthClaimValue( Value::OauthClaimValue(
"custom_a".to_string(), "custom_a".to_string(),
UUID_SYSTEM_ADMINS, UUID_TESTGROUP,
btreeset!["value_a".to_string()], btreeset!["value_a".to_string()],
), ),
), ),
@ -5320,7 +5603,7 @@ mod tests {
Attribute::OAuth2RsClaimMap.into(), Attribute::OAuth2RsClaimMap.into(),
Value::OauthClaimValue( Value::OauthClaimValue(
"custom_b".to_string(), "custom_b".to_string(),
UUID_SYSTEM_ADMINS, UUID_TESTGROUP,
btreeset!["value_a".to_string()], btreeset!["value_a".to_string()],
), ),
), ),
@ -5434,7 +5717,7 @@ mod tests {
== Url::parse("https://idm.example.com/oauth2/openid/test_resource_server") == Url::parse("https://idm.example.com/oauth2/openid/test_resource_server")
.unwrap() .unwrap()
); );
assert!(oidc.sub == OidcSubject::U(UUID_ADMIN)); assert!(oidc.sub == OidcSubject::U(UUID_TESTPERSON_1));
assert!(oidc.aud == "test_resource_server"); assert!(oidc.aud == "test_resource_server");
assert!(oidc.iat == iat); assert!(oidc.iat == iat);
assert!(oidc.nbf == Some(iat)); assert!(oidc.nbf == Some(iat));
@ -5448,8 +5731,8 @@ mod tests {
assert!(oidc.amr.is_none()); assert!(oidc.amr.is_none());
assert!(oidc.azp == Some("test_resource_server".to_string())); assert!(oidc.azp == Some("test_resource_server".to_string()));
assert!(oidc.jti.is_none()); assert!(oidc.jti.is_none());
assert!(oidc.s_claims.name == Some("System Administrator".to_string())); assert!(oidc.s_claims.name == Some("Test Person 1".to_string()));
assert!(oidc.s_claims.preferred_username == Some("admin@example.com".to_string())); assert!(oidc.s_claims.preferred_username == Some("testperson1@example.com".to_string()));
assert!( assert!(
oidc.s_claims.scopes == vec![OAUTH2_SCOPE_OPENID.to_string(), "supplement".to_string()] oidc.s_claims.scopes == vec![OAUTH2_SCOPE_OPENID.to_string(), "supplement".to_string()]
); );
@ -5498,7 +5781,7 @@ mod tests {
assert!(intr_response.active); assert!(intr_response.active);
assert!(intr_response.scope.as_deref() == Some("openid supplement")); assert!(intr_response.scope.as_deref() == Some("openid supplement"));
assert!(intr_response.client_id.as_deref() == Some("test_resource_server")); assert!(intr_response.client_id.as_deref() == Some("test_resource_server"));
assert!(intr_response.username.as_deref() == Some("admin@example.com")); assert!(intr_response.username.as_deref() == Some("testperson1@example.com"));
assert!(intr_response.token_type.as_deref() == Some("access_token")); assert!(intr_response.token_type.as_deref() == Some("access_token"));
assert!(intr_response.iat == Some(ct.as_secs() as i64)); assert!(intr_response.iat == Some(ct.as_secs() as i64));
assert!(intr_response.nbf == Some(ct.as_secs() as i64)); assert!(intr_response.nbf == Some(ct.as_secs() as i64));
@ -5596,4 +5879,161 @@ mod tests {
assert!(idms_prox_write.commit().is_ok()); assert!(idms_prox_write.commit().is_ok());
} }
#[idm_test]
async fn test_idm_oauth2_basic_client_credentials_grant_valid(
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, false).await;
let client_authz =
Some(general_purpose::STANDARD.encode(format!("test_resource_server:{secret}")));
// scope: Some(btreeset!["invalid_scope".to_string()]),
let mut idms_prox_write = idms.proxy_write(ct).await;
let token_req = AccessTokenRequest {
grant_type: GrantTypeReq::ClientCredentials { scope: None },
client_id: Some("test_resource_server".to_string()),
client_secret: Some(secret),
};
let oauth2_token = idms_prox_write
.check_oauth2_token_exchange(None, &token_req, ct)
.expect("Failed to perform OAuth2 token exchange");
assert!(idms_prox_write.commit().is_ok());
// 🎉 We got a token! In the future we can then check introspection from this point.
assert!(oauth2_token.token_type == "Bearer");
// Check Oauth2 Token Introspection
let mut idms_prox_read = idms.proxy_read().await;
let intr_request = AccessTokenIntrospectRequest {
token: oauth2_token.access_token.clone(),
token_type_hint: None,
};
let intr_response = idms_prox_read
.check_oauth2_token_introspect(client_authz.as_deref().unwrap(), &intr_request, ct)
.expect("Failed to inspect token");
eprintln!("👉 {intr_response:?}");
assert!(intr_response.active);
assert_eq!(intr_response.scope.as_deref(), Some("supplement"));
assert_eq!(
intr_response.client_id.as_deref(),
Some("test_resource_server")
);
assert_eq!(
intr_response.username.as_deref(),
Some("test_resource_server@example.com")
);
assert_eq!(intr_response.token_type.as_deref(), Some("access_token"));
assert_eq!(intr_response.iat, Some(ct.as_secs() as i64));
assert_eq!(intr_response.nbf, Some(ct.as_secs() as i64));
drop(idms_prox_read);
// Assert we can revoke.
let mut idms_prox_write = idms.proxy_write(ct).await;
let revoke_request = TokenRevokeRequest {
token: oauth2_token.access_token.clone(),
token_type_hint: None,
};
assert!(idms_prox_write
.oauth2_token_revoke(client_authz.as_deref().unwrap(), &revoke_request, ct,)
.is_ok());
assert!(idms_prox_write.commit().is_ok());
// Now must be invalid.
let ct = ct + GRACE_WINDOW;
let mut idms_prox_read = idms.proxy_read().await;
let intr_request = AccessTokenIntrospectRequest {
token: oauth2_token.access_token.clone(),
token_type_hint: None,
};
let intr_response = idms_prox_read
.check_oauth2_token_introspect(client_authz.as_deref().unwrap(), &intr_request, ct)
.expect("Failed to inspect token");
assert!(!intr_response.active);
drop(idms_prox_read);
}
#[idm_test]
async fn test_idm_oauth2_basic_client_credentials_grant_invalid(
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, false).await;
let mut idms_prox_write = idms.proxy_write(ct).await;
// Public Client
let token_req = AccessTokenRequest {
grant_type: GrantTypeReq::ClientCredentials { scope: None },
client_id: Some("test_resource_server".to_string()),
client_secret: None,
};
assert_eq!(
idms_prox_write
.check_oauth2_token_exchange(None, &token_req, ct)
.unwrap_err(),
Oauth2Error::AuthenticationRequired
);
// Incorrect Password
let token_req = AccessTokenRequest {
grant_type: GrantTypeReq::ClientCredentials { scope: None },
client_id: Some("test_resource_server".to_string()),
client_secret: Some("wrong password".to_string()),
};
assert_eq!(
idms_prox_write
.check_oauth2_token_exchange(None, &token_req, ct)
.unwrap_err(),
Oauth2Error::AuthenticationRequired
);
// Invalid scope
let scope = Some(btreeset!["💅".to_string()]);
let token_req = AccessTokenRequest {
grant_type: GrantTypeReq::ClientCredentials { scope },
client_id: Some("test_resource_server".to_string()),
client_secret: Some(secret.clone()),
};
assert_eq!(
idms_prox_write
.check_oauth2_token_exchange(None, &token_req, ct)
.unwrap_err(),
Oauth2Error::InvalidScope
);
// Scopes we aren't a member-of
let scope = Some(btreeset!["invalid_scope".to_string()]);
let token_req = AccessTokenRequest {
grant_type: GrantTypeReq::ClientCredentials { scope },
client_id: Some("test_resource_server".to_string()),
client_secret: Some(secret.clone()),
};
assert_eq!(
idms_prox_write
.check_oauth2_token_exchange(None, &token_req, ct)
.unwrap_err(),
Oauth2Error::AccessDenied
);
assert!(idms_prox_write.commit().is_ok());
}
} }

View file

@ -628,7 +628,7 @@ pub trait IdmServerTransaction<'a> {
&mut self, &mut self,
uuid: Uuid, uuid: Uuid,
session_id: Uuid, session_id: Uuid,
parent_session_id: Uuid, parent_session_id: Option<Uuid>,
iat: i64, iat: i64,
ct: Duration, ct: Duration,
) -> Result<Option<Arc<Entry<EntrySealed, EntryCommitted>>>, OperationError> { ) -> Result<Option<Arc<Entry<EntrySealed, EntryCommitted>>>, OperationError> {
@ -661,9 +661,6 @@ pub trait IdmServerTransaction<'a> {
let oauth2_session = entry let oauth2_session = entry
.get_ava_as_oauth2session_map(Attribute::OAuth2Session) .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
.and_then(|sessions| sessions.get(&session_id)); .and_then(|sessions| sessions.get(&session_id));
let uat_session = entry
.get_ava_as_session_map(Attribute::UserAuthTokenSession)
.and_then(|sessions| sessions.get(&parent_session_id));
if let Some(oauth2_session) = oauth2_session { if let Some(oauth2_session) = oauth2_session {
// We have the oauth2 session, lets check it. // We have the oauth2 session, lets check it.
@ -674,10 +671,19 @@ pub trait IdmServerTransaction<'a> {
return Ok(None); return Ok(None);
} }
// Do we have a parent session? If yes, we need to enforce it's presence.
if let Some(parent_session_id) = parent_session_id {
let uat_session = entry
.get_ava_as_session_map(Attribute::UserAuthTokenSession)
.and_then(|sessions| sessions.get(&parent_session_id));
if let Some(uat_session) = uat_session { if let Some(uat_session) = uat_session {
let parent_session_valid = !matches!(uat_session.state, SessionState::RevokedAt(_)); let parent_session_valid =
!matches!(uat_session.state, SessionState::RevokedAt(_));
if parent_session_valid { if parent_session_valid {
security_info!("A valid parent and oauth2 session value exists for this token"); security_info!(
"A valid parent and oauth2 session value exists for this token"
);
} else { } else {
security_info!( security_info!(
"The parent oauth2 session associated to this token is revoked." "The parent oauth2 session associated to this token is revoked."
@ -692,6 +698,8 @@ pub trait IdmServerTransaction<'a> {
security_info!("The token grace window has passed and no entry parent sessions exist. Assuming invalid."); security_info!("The token grace window has passed and no entry parent sessions exist. Assuming invalid.");
return Ok(None); return Ok(None);
} }
}
// If we don't have a parent session id, we are good to proceed.
} else if grace_valid { } else if grace_valid {
security_info!("The token grace window is in effect. Assuming valid."); security_info!("The token grace window is in effect. Assuming valid.");
} else { } else {
@ -2332,16 +2340,24 @@ mod tests {
} }
} }
async fn init_admin_w_password(idms: &IdmServer, pw: &str) -> Result<Uuid, OperationError> { async fn init_testperson_w_password(
idms: &IdmServer,
pw: &str,
) -> Result<Uuid, OperationError> {
let p = CryptoPolicy::minimum(); let p = CryptoPolicy::minimum();
let cred = Credential::new_password_only(&p, pw)?; let cred = Credential::new_password_only(&p, pw)?;
let cred_id = cred.uuid; let cred_id = cred.uuid;
let v_cred = Value::new_credential("primary", cred); let v_cred = Value::new_credential("primary", cred);
let mut idms_write = idms.proxy_write(duration_from_epoch_now()).await; let mut idms_write = idms.proxy_write(duration_from_epoch_now()).await;
idms_write
.qs_write
.internal_create(vec![E_TESTPERSON_1.clone()])
.expect("Failed to create test person");
// now modify and provide a primary credential. // now modify and provide a primary credential.
let me_inv_m = ModifyEvent::new_internal_invalid( let me_inv_m = ModifyEvent::new_internal_invalid(
filter!(f_eq(Attribute::Name, PartialValue::new_iname("admin"))), filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
ModifyList::new_list(vec![Modify::Present( ModifyList::new_list(vec![Modify::Present(
Attribute::PrimaryCredential.into(), Attribute::PrimaryCredential.into(),
v_cred, v_cred,
@ -2353,7 +2369,7 @@ mod tests {
idms_write.commit().map(|()| cred_id) idms_write.commit().map(|()| cred_id)
} }
async fn init_admin_authsession_sid(idms: &IdmServer, ct: Duration, name: &str) -> Uuid { async fn init_authsession_sid(idms: &IdmServer, ct: Duration, name: &str) -> Uuid {
let mut idms_auth = idms.auth().await; let mut idms_auth = idms.auth().await;
let admin_init = AuthEvent::named_init(name); let admin_init = AuthEvent::named_init(name);
@ -2387,9 +2403,9 @@ mod tests {
sessionid sessionid
} }
async fn check_admin_password(idms: &IdmServer, pw: &str) -> String { async fn check_testperson_password(idms: &IdmServer, pw: &str) -> String {
let sid = let sid =
init_admin_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "admin").await; init_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "testperson1").await;
let mut idms_auth = idms.auth().await; let mut idms_auth = idms.auth().await;
let anon_step = AuthEvent::cred_step_password(sid, pw); let anon_step = AuthEvent::cred_step_password(sid, pw);
@ -2436,10 +2452,10 @@ mod tests {
#[idm_test] #[idm_test]
async fn test_idm_simple_password_auth(idms: &IdmServer, idms_delayed: &mut IdmServerDelayed) { async fn test_idm_simple_password_auth(idms: &IdmServer, idms_delayed: &mut IdmServerDelayed) {
init_admin_w_password(idms, TEST_PASSWORD) init_testperson_w_password(idms, TEST_PASSWORD)
.await .await
.expect("Failed to setup admin account"); .expect("Failed to setup admin account");
check_admin_password(idms, TEST_PASSWORD).await; check_testperson_password(idms, TEST_PASSWORD).await;
// Clear our the session record // Clear our the session record
let da = idms_delayed.try_recv().expect("invalid"); let da = idms_delayed.try_recv().expect("invalid");
@ -2452,14 +2468,14 @@ mod tests {
idms: &IdmServer, idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed, idms_delayed: &mut IdmServerDelayed,
) { ) {
init_admin_w_password(idms, TEST_PASSWORD) init_testperson_w_password(idms, TEST_PASSWORD)
.await .await
.expect("Failed to setup admin account"); .expect("Failed to setup admin account");
let sid = init_admin_authsession_sid( let sid = init_authsession_sid(
idms, idms,
Duration::from_secs(TEST_CURRENT_TIME), Duration::from_secs(TEST_CURRENT_TIME),
"admin@example.com", "testperson1@example.com",
) )
.await; .await;
@ -2513,11 +2529,11 @@ mod tests {
_idms_delayed: &IdmServerDelayed, _idms_delayed: &IdmServerDelayed,
idms_audit: &mut IdmServerAudit, idms_audit: &mut IdmServerAudit,
) { ) {
init_admin_w_password(idms, TEST_PASSWORD) init_testperson_w_password(idms, TEST_PASSWORD)
.await .await
.expect("Failed to setup admin account"); .expect("Failed to setup admin account");
let sid = let sid =
init_admin_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "admin").await; init_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "testperson1").await;
let mut idms_auth = idms.auth().await; let mut idms_auth = idms.auth().await;
let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD_INC); let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD_INC);
@ -2588,7 +2604,13 @@ mod tests {
#[idm_test] #[idm_test]
async fn test_idm_regenerate_radius_secret(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { async fn test_idm_regenerate_radius_secret(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await; let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await;
let rrse = RegenerateRadiusSecretEvent::new_internal(UUID_ADMIN);
idms_prox_write
.qs_write
.internal_create(vec![E_TESTPERSON_1.clone()])
.expect("unable to create test person");
let rrse = RegenerateRadiusSecretEvent::new_internal(UUID_TESTPERSON_1);
// Generates a new credential when none exists // Generates a new credential when none exists
let r1 = idms_prox_write let r1 = idms_prox_write
@ -2604,19 +2626,25 @@ mod tests {
#[idm_test] #[idm_test]
async fn test_idm_radiusauthtoken(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { async fn test_idm_radiusauthtoken(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await; let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await;
let rrse = RegenerateRadiusSecretEvent::new_internal(UUID_ADMIN);
idms_prox_write
.qs_write
.internal_create(vec![E_TESTPERSON_1.clone()])
.expect("unable to create test person");
let rrse = RegenerateRadiusSecretEvent::new_internal(UUID_TESTPERSON_1);
let r1 = idms_prox_write let r1 = idms_prox_write
.regenerate_radius_secret(&rrse) .regenerate_radius_secret(&rrse)
.expect("Failed to reset radius credential 1"); .expect("Failed to reset radius credential 1");
idms_prox_write.commit().expect("failed to commit"); idms_prox_write.commit().expect("failed to commit");
let mut idms_prox_read = idms.proxy_read().await; let mut idms_prox_read = idms.proxy_read().await;
let admin_entry = idms_prox_read let person_entry = idms_prox_read
.qs_read .qs_read
.internal_search_uuid(UUID_ADMIN) .internal_search_uuid(UUID_TESTPERSON_1)
.expect("Can't access admin entry."); .expect("Can't access admin entry.");
let rate = RadiusAuthTokenEvent::new_impersonate(admin_entry, UUID_ADMIN); let rate = RadiusAuthTokenEvent::new_impersonate(person_entry, UUID_TESTPERSON_1);
let tok_r = idms_prox_read let tok_r = idms_prox_read
.get_radiusauthtoken(&rate, duration_from_epoch_now()) .get_radiusauthtoken(&rate, duration_from_epoch_now())
.expect("Failed to generate radius auth token"); .expect("Failed to generate radius auth token");
@ -2780,9 +2808,15 @@ mod tests {
{ {
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await; let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await;
// now modify and provide a primary credential. // now modify and provide a primary credential.
idms_prox_write
.qs_write
.internal_create(vec![E_TESTPERSON_1.clone()])
.expect("Failed to create test person");
let me_inv_m = let me_inv_m =
ModifyEvent::new_internal_invalid( ModifyEvent::new_internal_invalid(
filter!(f_eq(Attribute::Name, PartialValue::new_iname("admin"))), filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
ModifyList::new_list(vec![Modify::Present( ModifyList::new_list(vec![Modify::Present(
Attribute::PasswordImport.into(), Attribute::PasswordImport.into(),
Value::from("{SSHA512}JwrSUHkI7FTAfHRVR6KoFlSN0E3dmaQWARjZ+/UsShYlENOqDtFVU77HJLLrY2MuSp0jve52+pwtdVl2QUAHukQ0XUf5LDtM") Value::from("{SSHA512}JwrSUHkI7FTAfHRVR6KoFlSN0E3dmaQWARjZ+/UsShYlENOqDtFVU77HJLLrY2MuSp0jve52+pwtdVl2QUAHukQ0XUf5LDtM")
@ -2796,18 +2830,18 @@ mod tests {
idms_delayed.check_is_empty_or_panic(); idms_delayed.check_is_empty_or_panic();
let mut idms_prox_read = idms.proxy_read().await; let mut idms_prox_read = idms.proxy_read().await;
let admin_entry = idms_prox_read let person_entry = idms_prox_read
.qs_read .qs_read
.internal_search_uuid(UUID_ADMIN) .internal_search_uuid(UUID_TESTPERSON_1)
.expect("Can't access admin entry."); .expect("Can't access admin entry.");
let cred_before = admin_entry let cred_before = person_entry
.get_ava_single_credential(Attribute::PrimaryCredential) .get_ava_single_credential(Attribute::PrimaryCredential)
.expect("No credential present") .expect("No credential present")
.clone(); .clone();
drop(idms_prox_read); drop(idms_prox_read);
// Do an auth, this will trigger the action to send. // Do an auth, this will trigger the action to send.
check_admin_password(idms, "password").await; check_testperson_password(idms, "password").await;
// ⚠️ We have to be careful here. Between these two actions, it's possible // ⚠️ We have to be careful here. Between these two actions, it's possible
// that on the pw upgrade that the credential uuid changes. This immediately // that on the pw upgrade that the credential uuid changes. This immediately
@ -2827,11 +2861,11 @@ mod tests {
assert!(Ok(true) == r); assert!(Ok(true) == r);
let mut idms_prox_read = idms.proxy_read().await; let mut idms_prox_read = idms.proxy_read().await;
let admin_entry = idms_prox_read let person_entry = idms_prox_read
.qs_read .qs_read
.internal_search_uuid(UUID_ADMIN) .internal_search_uuid(UUID_TESTPERSON_1)
.expect("Can't access admin entry."); .expect("Can't access admin entry.");
let cred_after = admin_entry let cred_after = person_entry
.get_ava_single_credential(Attribute::PrimaryCredential) .get_ava_single_credential(Attribute::PrimaryCredential)
.expect("No credential present") .expect("No credential present")
.clone(); .clone();
@ -2840,7 +2874,7 @@ mod tests {
assert_eq!(cred_before.uuid, cred_after.uuid); assert_eq!(cred_before.uuid, cred_after.uuid);
// Check the admin pw still matches // Check the admin pw still matches
check_admin_password(idms, "password").await; check_testperson_password(idms, "password").await;
// Clear the next auth session record // Clear the next auth session record
let da = idms_delayed.try_recv().expect("invalid"); let da = idms_delayed.try_recv().expect("invalid");
assert!(matches!(da, DelayedAction::AuthSessionRecord(_))); assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
@ -2909,7 +2943,7 @@ mod tests {
const TEST_EXPIRE_TIME: u64 = TEST_CURRENT_TIME + 120; const TEST_EXPIRE_TIME: u64 = TEST_CURRENT_TIME + 120;
const TEST_AFTER_EXPIRY: u64 = TEST_CURRENT_TIME + 240; const TEST_AFTER_EXPIRY: u64 = TEST_CURRENT_TIME + 240;
async fn set_admin_valid_time(idms: &IdmServer) { async fn set_testperson_valid_time(idms: &IdmServer) {
let mut idms_write = idms.proxy_write(duration_from_epoch_now()).await; let mut idms_write = idms.proxy_write(duration_from_epoch_now()).await;
let v_valid_from = Value::new_datetime_epoch(Duration::from_secs(TEST_VALID_FROM_TIME)); let v_valid_from = Value::new_datetime_epoch(Duration::from_secs(TEST_VALID_FROM_TIME));
@ -2917,7 +2951,7 @@ mod tests {
// now modify and provide a primary credential. // now modify and provide a primary credential.
let me_inv_m = ModifyEvent::new_internal_invalid( let me_inv_m = ModifyEvent::new_internal_invalid(
filter!(f_eq(Attribute::Name, PartialValue::new_iname("admin"))), filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
ModifyList::new_list(vec![ ModifyList::new_list(vec![
Modify::Present(Attribute::AccountExpire.into(), v_expire), Modify::Present(Attribute::AccountExpire.into(), v_expire),
Modify::Present(Attribute::AccountValidFrom.into(), v_valid_from), Modify::Present(Attribute::AccountValidFrom.into(), v_valid_from),
@ -2936,12 +2970,12 @@ mod tests {
) { ) {
// Any account that is not yet valrid / expired can't auth. // Any account that is not yet valrid / expired can't auth.
init_admin_w_password(idms, TEST_PASSWORD) init_testperson_w_password(idms, TEST_PASSWORD)
.await .await
.expect("Failed to setup admin account"); .expect("Failed to setup admin account");
// Set the valid bounds high/low // Set the valid bounds high/low
// TEST_VALID_FROM_TIME/TEST_EXPIRE_TIME // TEST_VALID_FROM_TIME/TEST_EXPIRE_TIME
set_admin_valid_time(idms).await; set_testperson_valid_time(idms).await;
let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME); let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME);
let time_high = Duration::from_secs(TEST_AFTER_EXPIRY); let time_high = Duration::from_secs(TEST_AFTER_EXPIRY);
@ -2996,10 +3030,10 @@ mod tests {
_idms_delayed: &mut IdmServerDelayed, _idms_delayed: &mut IdmServerDelayed,
) { ) {
// Any account that is expired can't unix auth. // Any account that is expired can't unix auth.
init_admin_w_password(idms, TEST_PASSWORD) init_testperson_w_password(idms, TEST_PASSWORD)
.await .await
.expect("Failed to setup admin account"); .expect("Failed to setup admin account");
set_admin_valid_time(idms).await; set_testperson_valid_time(idms).await;
let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME); let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME);
let time_high = Duration::from_secs(TEST_AFTER_EXPIRY); let time_high = Duration::from_secs(TEST_AFTER_EXPIRY);
@ -3007,7 +3041,7 @@ mod tests {
// make the admin a valid posix account // make the admin a valid posix account
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await; let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await;
let me_posix = ModifyEvent::new_internal_invalid( let me_posix = ModifyEvent::new_internal_invalid(
filter!(f_eq(Attribute::Name, PartialValue::new_iname("admin"))), filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
ModifyList::new_list(vec![ ModifyList::new_list(vec![
Modify::Present(Attribute::Class.into(), EntryClass::PosixAccount.into()), Modify::Present(Attribute::Class.into(), EntryClass::PosixAccount.into()),
Modify::Present(Attribute::GidNumber.into(), Value::new_uint32(2001)), Modify::Present(Attribute::GidNumber.into(), Value::new_uint32(2001)),
@ -3015,14 +3049,14 @@ mod tests {
); );
assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok()); assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());
let pce = UnixPasswordChangeEvent::new_internal(UUID_ADMIN, TEST_PASSWORD); let pce = UnixPasswordChangeEvent::new_internal(UUID_TESTPERSON_1, TEST_PASSWORD);
assert!(idms_prox_write.set_unix_account_password(&pce).is_ok()); assert!(idms_prox_write.set_unix_account_password(&pce).is_ok());
assert!(idms_prox_write.commit().is_ok()); assert!(idms_prox_write.commit().is_ok());
// Now check auth when the time is too high or too low. // Now check auth when the time is too high or too low.
let mut idms_auth = idms.auth().await; let mut idms_auth = idms.auth().await;
let uuae_good = UnixUserAuthEvent::new_internal(UUID_ADMIN, TEST_PASSWORD); let uuae_good = UnixUserAuthEvent::new_internal(UUID_TESTPERSON_1, TEST_PASSWORD);
let a1 = idms_auth.auth_unix(&uuae_good, time_low).await; let a1 = idms_auth.auth_unix(&uuae_good, time_low).await;
// Should this actually send an error with the details? Or just silently act as // Should this actually send an error with the details? Or just silently act as
@ -3041,20 +3075,20 @@ mod tests {
idms_auth.commit().expect("Must not fail"); idms_auth.commit().expect("Must not fail");
// Also check the generated unix tokens are invalid. // Also check the generated unix tokens are invalid.
let mut idms_prox_read = idms.proxy_read().await; let mut idms_prox_read = idms.proxy_read().await;
let uute = UnixUserTokenEvent::new_internal(UUID_ADMIN); let uute = UnixUserTokenEvent::new_internal(UUID_TESTPERSON_1);
let tok_r = idms_prox_read let tok_r = idms_prox_read
.get_unixusertoken(&uute, time_low) .get_unixusertoken(&uute, time_low)
.expect("Failed to generate unix user token"); .expect("Failed to generate unix user token");
assert!(tok_r.name == "admin"); assert!(tok_r.name == "testperson1");
assert!(!tok_r.valid); assert!(!tok_r.valid);
let tok_r = idms_prox_read let tok_r = idms_prox_read
.get_unixusertoken(&uute, time_high) .get_unixusertoken(&uute, time_high)
.expect("Failed to generate unix user token"); .expect("Failed to generate unix user token");
assert!(tok_r.name == "admin"); assert!(tok_r.name == "testperson1");
assert!(!tok_r.valid); assert!(!tok_r.valid);
} }
@ -3065,16 +3099,16 @@ mod tests {
) { ) {
// Any account not valid/expiry should not return // Any account not valid/expiry should not return
// a radius packet. // a radius packet.
init_admin_w_password(idms, TEST_PASSWORD) init_testperson_w_password(idms, TEST_PASSWORD)
.await .await
.expect("Failed to setup admin account"); .expect("Failed to setup admin account");
set_admin_valid_time(idms).await; set_testperson_valid_time(idms).await;
let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME); let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME);
let time_high = Duration::from_secs(TEST_AFTER_EXPIRY); let time_high = Duration::from_secs(TEST_AFTER_EXPIRY);
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await; let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await;
let rrse = RegenerateRadiusSecretEvent::new_internal(UUID_ADMIN); let rrse = RegenerateRadiusSecretEvent::new_internal(UUID_TESTPERSON_1);
let _r1 = idms_prox_write let _r1 = idms_prox_write
.regenerate_radius_secret(&rrse) .regenerate_radius_secret(&rrse)
.expect("Failed to reset radius credential 1"); .expect("Failed to reset radius credential 1");
@ -3110,13 +3144,13 @@ mod tests {
idms_delayed: &mut IdmServerDelayed, idms_delayed: &mut IdmServerDelayed,
idms_audit: &mut IdmServerAudit, idms_audit: &mut IdmServerAudit,
) { ) {
init_admin_w_password(idms, TEST_PASSWORD) init_testperson_w_password(idms, TEST_PASSWORD)
.await .await
.expect("Failed to setup admin account"); .expect("Failed to setup admin account");
// Auth invalid, no softlock present. // Auth invalid, no softlock present.
let sid = let sid =
init_admin_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "admin").await; init_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "testperson1").await;
let mut idms_auth = idms.auth().await; let mut idms_auth = idms.auth().await;
let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD_INC); let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD_INC);
@ -3163,7 +3197,7 @@ mod tests {
// aka Auth valid immediate, (ct < exp), autofail // aka Auth valid immediate, (ct < exp), autofail
// aka Auth invalid immediate, (ct < exp), autofail // aka Auth invalid immediate, (ct < exp), autofail
let mut idms_auth = idms.auth().await; let mut idms_auth = idms.auth().await;
let admin_init = AuthEvent::named_init("admin"); let admin_init = AuthEvent::named_init("testperson1");
let r1 = idms_auth let r1 = idms_auth
.auth( .auth(
@ -3208,8 +3242,11 @@ mod tests {
// Tested in the softlock state machine. // Tested in the softlock state machine.
// Auth valid once softlock pass, valid. Count remains. // Auth valid once softlock pass, valid. Count remains.
let sid = let sid = init_authsession_sid(
init_admin_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME + 2), "admin") idms,
Duration::from_secs(TEST_CURRENT_TIME + 2),
"testperson1",
)
.await; .await;
let mut idms_auth = idms.auth().await; let mut idms_auth = idms.auth().await;
@ -3269,17 +3306,17 @@ mod tests {
_idms_delayed: &mut IdmServerDelayed, _idms_delayed: &mut IdmServerDelayed,
idms_audit: &mut IdmServerAudit, idms_audit: &mut IdmServerAudit,
) { ) {
init_admin_w_password(idms, TEST_PASSWORD) init_testperson_w_password(idms, TEST_PASSWORD)
.await .await
.expect("Failed to setup admin account"); .expect("Failed to setup admin account");
// Start an *early* auth session. // Start an *early* auth session.
let sid_early = let sid_early =
init_admin_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "admin").await; init_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "testperson1").await;
// Start a second auth session // Start a second auth session
let sid_later = let sid_later =
init_admin_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "admin").await; init_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "testperson1").await;
// Get the detail wrong in sid_later. // Get the detail wrong in sid_later.
let mut idms_auth = idms.auth().await; let mut idms_auth = idms.auth().await;
let anon_step = AuthEvent::cred_step_password(sid_later, TEST_PASSWORD_INC); let anon_step = AuthEvent::cred_step_password(sid_later, TEST_PASSWORD_INC);
@ -3364,13 +3401,13 @@ mod tests {
idms: &IdmServer, idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed, _idms_delayed: &mut IdmServerDelayed,
) { ) {
init_admin_w_password(idms, TEST_PASSWORD) init_testperson_w_password(idms, TEST_PASSWORD)
.await .await
.expect("Failed to setup admin account"); .expect("Failed to setup admin account");
// make the admin a valid posix account // make the admin a valid posix account
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await; let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await;
let me_posix = ModifyEvent::new_internal_invalid( let me_posix = ModifyEvent::new_internal_invalid(
filter!(f_eq(Attribute::Name, PartialValue::new_iname("admin"))), filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
ModifyList::new_list(vec![ ModifyList::new_list(vec![
Modify::Present(Attribute::Class.into(), EntryClass::PosixAccount.into()), Modify::Present(Attribute::Class.into(), EntryClass::PosixAccount.into()),
Modify::Present(Attribute::GidNumber.into(), Value::new_uint32(2001)), Modify::Present(Attribute::GidNumber.into(), Value::new_uint32(2001)),
@ -3378,13 +3415,13 @@ mod tests {
); );
assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok()); assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());
let pce = UnixPasswordChangeEvent::new_internal(UUID_ADMIN, TEST_PASSWORD); let pce = UnixPasswordChangeEvent::new_internal(UUID_TESTPERSON_1, TEST_PASSWORD);
assert!(idms_prox_write.set_unix_account_password(&pce).is_ok()); assert!(idms_prox_write.set_unix_account_password(&pce).is_ok());
assert!(idms_prox_write.commit().is_ok()); assert!(idms_prox_write.commit().is_ok());
let mut idms_auth = idms.auth().await; let mut idms_auth = idms.auth().await;
let uuae_good = UnixUserAuthEvent::new_internal(UUID_ADMIN, TEST_PASSWORD); let uuae_good = UnixUserAuthEvent::new_internal(UUID_TESTPERSON_1, TEST_PASSWORD);
let uuae_bad = UnixUserAuthEvent::new_internal(UUID_ADMIN, TEST_PASSWORD_INC); let uuae_bad = UnixUserAuthEvent::new_internal(UUID_TESTPERSON_1, TEST_PASSWORD_INC);
let a2 = idms_auth let a2 = idms_auth
.auth_unix(&uuae_bad, Duration::from_secs(TEST_CURRENT_TIME)) .auth_unix(&uuae_bad, Duration::from_secs(TEST_CURRENT_TIME))
@ -3420,10 +3457,10 @@ mod tests {
let ct = Duration::from_secs(TEST_CURRENT_TIME); let ct = Duration::from_secs(TEST_CURRENT_TIME);
let expiry = ct + Duration::from_secs((DEFAULT_AUTH_SESSION_EXPIRY + 1).into()); let expiry = ct + Duration::from_secs((DEFAULT_AUTH_SESSION_EXPIRY + 1).into());
// Do an authenticate // Do an authenticate
init_admin_w_password(idms, TEST_PASSWORD) init_testperson_w_password(idms, TEST_PASSWORD)
.await .await
.expect("Failed to setup admin account"); .expect("Failed to setup admin account");
let token = check_admin_password(idms, TEST_PASSWORD).await; let token = check_testperson_password(idms, TEST_PASSWORD).await;
// Clear out the queued session record // Clear out the queued session record
let da = idms_delayed.try_recv().expect("invalid"); let da = idms_delayed.try_recv().expect("invalid");
@ -3460,7 +3497,7 @@ mod tests {
let session_b = Uuid::new_v4(); let session_b = Uuid::new_v4();
// We need to put the credential on the admin. // We need to put the credential on the admin.
let cred_id = init_admin_w_password(idms, TEST_PASSWORD) let cred_id = init_testperson_w_password(idms, TEST_PASSWORD)
.await .await
.expect("Failed to setup admin account"); .expect("Failed to setup admin account");
@ -3468,14 +3505,14 @@ mod tests {
let mut idms_prox_read = idms.proxy_read().await; let mut idms_prox_read = idms.proxy_read().await;
let admin = idms_prox_read let admin = idms_prox_read
.qs_read .qs_read
.internal_search_uuid(UUID_ADMIN) .internal_search_uuid(UUID_TESTPERSON_1)
.expect("failed"); .expect("failed");
let sessions = admin.get_ava_as_session_map(Attribute::UserAuthTokenSession); let sessions = admin.get_ava_as_session_map(Attribute::UserAuthTokenSession);
assert!(sessions.is_none()); assert!(sessions.is_none());
drop(idms_prox_read); drop(idms_prox_read);
let da = DelayedAction::AuthSessionRecord(AuthSessionRecord { let da = DelayedAction::AuthSessionRecord(AuthSessionRecord {
target_uuid: UUID_ADMIN, target_uuid: UUID_TESTPERSON_1,
session_id: session_a, session_id: session_a,
cred_id, cred_id,
label: "Test Session A".to_string(), label: "Test Session A".to_string(),
@ -3492,7 +3529,7 @@ mod tests {
let mut idms_prox_read = idms.proxy_read().await; let mut idms_prox_read = idms.proxy_read().await;
let admin = idms_prox_read let admin = idms_prox_read
.qs_read .qs_read
.internal_search_uuid(UUID_ADMIN) .internal_search_uuid(UUID_TESTPERSON_1)
.expect("failed"); .expect("failed");
let sessions = admin let sessions = admin
.get_ava_as_session_map(Attribute::UserAuthTokenSession) .get_ava_as_session_map(Attribute::UserAuthTokenSession)
@ -3506,7 +3543,7 @@ mod tests {
// When we re-auth, this is what triggers the session revoke via the delayed action. // When we re-auth, this is what triggers the session revoke via the delayed action.
let da = DelayedAction::AuthSessionRecord(AuthSessionRecord { let da = DelayedAction::AuthSessionRecord(AuthSessionRecord {
target_uuid: UUID_ADMIN, target_uuid: UUID_TESTPERSON_1,
session_id: session_b, session_id: session_b,
cred_id, cred_id,
label: "Test Session B".to_string(), label: "Test Session B".to_string(),
@ -3522,7 +3559,7 @@ mod tests {
let mut idms_prox_read = idms.proxy_read().await; let mut idms_prox_read = idms.proxy_read().await;
let admin = idms_prox_read let admin = idms_prox_read
.qs_read .qs_read
.internal_search_uuid(UUID_ADMIN) .internal_search_uuid(UUID_TESTPERSON_1)
.expect("failed"); .expect("failed");
let sessions = admin let sessions = admin
.get_ava_as_session_map(Attribute::UserAuthTokenSession) .get_ava_as_session_map(Attribute::UserAuthTokenSession)
@ -3556,10 +3593,10 @@ mod tests {
assert!(post_grace < expiry); assert!(post_grace < expiry);
// Do an authenticate // Do an authenticate
init_admin_w_password(idms, TEST_PASSWORD) init_testperson_w_password(idms, TEST_PASSWORD)
.await .await
.expect("Failed to setup admin account"); .expect("Failed to setup admin account");
let token = check_admin_password(idms, TEST_PASSWORD).await; let token = check_testperson_password(idms, TEST_PASSWORD).await;
// Process the session info. // Process the session info.
let da = idms_delayed.try_recv().expect("invalid"); let da = idms_delayed.try_recv().expect("invalid");
@ -3922,10 +3959,10 @@ mod tests {
) { ) {
let ct = Duration::from_secs(TEST_CURRENT_TIME); let ct = Duration::from_secs(TEST_CURRENT_TIME);
init_admin_w_password(idms, TEST_PASSWORD) init_testperson_w_password(idms, TEST_PASSWORD)
.await .await
.expect("Failed to setup admin account"); .expect("Failed to setup admin account");
let token = check_admin_password(idms, TEST_PASSWORD).await; let token = check_testperson_password(idms, TEST_PASSWORD).await;
// Clear the session record // Clear the session record
let da = idms_delayed.try_recv().expect("invalid"); let da = idms_delayed.try_recv().expect("invalid");
@ -3958,7 +3995,7 @@ mod tests {
assert!(idms_prox_write.qs_write.modify(&me_reset_tokens).is_ok()); assert!(idms_prox_write.qs_write.modify(&me_reset_tokens).is_ok());
assert!(idms_prox_write.commit().is_ok()); assert!(idms_prox_write.commit().is_ok());
// Check the old token is invalid, due to reload. // Check the old token is invalid, due to reload.
let new_token = check_admin_password(idms, TEST_PASSWORD).await; let new_token = check_testperson_password(idms, TEST_PASSWORD).await;
// Clear the session record // Clear the session record
let da = idms_delayed.try_recv().expect("invalid"); let da = idms_delayed.try_recv().expect("invalid");

View file

@ -500,6 +500,7 @@ mod tests {
fn test_pre_create_name_unique() { fn test_pre_create_name_unique() {
let e: Entry<EntryInit, EntryNew> = entry_init!( let e: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Name, Value::new_iname("testperson")), (Attribute::Name, Value::new_iname("testperson")),
(Attribute::Description, Value::new_utf8s("testperson")), (Attribute::Description, Value::new_utf8s("testperson")),
(Attribute::DisplayName, Value::new_utf8s("testperson")) (Attribute::DisplayName, Value::new_utf8s("testperson"))
@ -524,6 +525,7 @@ mod tests {
fn test_pre_create_name_unique_2() { fn test_pre_create_name_unique_2() {
let e: Entry<EntryInit, EntryNew> = entry_init!( let e: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Name, Value::new_iname("testperson")), (Attribute::Name, Value::new_iname("testperson")),
(Attribute::Description, Value::new_utf8s("testperson")), (Attribute::Description, Value::new_utf8s("testperson")),
(Attribute::DisplayName, Value::new_utf8s("testperson")) (Attribute::DisplayName, Value::new_utf8s("testperson"))

View file

@ -333,7 +333,7 @@ mod tests {
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
r#"{ r#"{
"attrs": { "attrs": {
"class": ["person"], "class": ["person", "account"],
"name": ["testperson"], "name": ["testperson"],
"description": ["testperson"], "description": ["testperson"],
"displayname": ["testperson"] "displayname": ["testperson"]
@ -369,7 +369,7 @@ mod tests {
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
r#"{ r#"{
"attrs": { "attrs": {
"class": ["person"], "class": ["person", "account"],
"name": ["testperson"], "name": ["testperson"],
"description": ["testperson"], "description": ["testperson"],
"displayname": ["testperson"], "displayname": ["testperson"],
@ -399,7 +399,7 @@ mod tests {
let mut e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( let mut e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
r#"{ r#"{
"attrs": { "attrs": {
"class": ["person"], "class": ["person", "account"],
"name": ["testperson"], "name": ["testperson"],
"description": ["testperson"], "description": ["testperson"],
"displayname": ["testperson"], "displayname": ["testperson"],
@ -432,7 +432,7 @@ mod tests {
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
r#"{ r#"{
"attrs": { "attrs": {
"class": ["person"], "class": ["person", "account"],
"name": ["testperson"], "name": ["testperson"],
"description": ["testperson"], "description": ["testperson"],
"displayname": ["testperson"], "displayname": ["testperson"],
@ -506,7 +506,7 @@ mod tests {
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
r#"{ r#"{
"attrs": { "attrs": {
"class": ["person"], "class": ["account", "person"],
"name": ["testperson"], "name": ["testperson"],
"description": ["testperson"], "description": ["testperson"],
"displayname": ["testperson"], "displayname": ["testperson"],

View file

@ -104,7 +104,7 @@ impl Domain {
// Setup the minimum functional level if one is not set already. // Setup the minimum functional level if one is not set already.
if !e.attribute_pres(Attribute::Version) { if !e.attribute_pres(Attribute::Version) {
let n = Value::Uint32(DOMAIN_MIN_LEVEL); let n = Value::Uint32(DOMAIN_LEVEL_0);
e.set_ava(Attribute::Version, once(n)); e.set_ava(Attribute::Version, once(n));
warn!("plugin_domain: Applying domain version transform"); warn!("plugin_domain: Applying domain version transform");
} else { } else {

View file

@ -244,6 +244,7 @@ impl DynGroup {
pre_cand: &[Arc<Entry<EntrySealed, EntryCommitted>>], pre_cand: &[Arc<Entry<EntrySealed, EntryCommitted>>],
cand: &[Entry<EntrySealed, EntryCommitted>], cand: &[Entry<EntrySealed, EntryCommitted>],
_ident: &Identity, _ident: &Identity,
force_cand_updates: bool,
) -> Result<Vec<Uuid>, OperationError> { ) -> Result<Vec<Uuid>, OperationError> {
let mut affected_uuids = Vec::with_capacity(cand.len()); let mut affected_uuids = Vec::with_capacity(cand.len());
@ -290,8 +291,7 @@ impl DynGroup {
// If we modified anything else, check if a dyngroup is affected by it's change // If we modified anything else, check if a dyngroup is affected by it's change
// if it was a member. // if it was a member.
trace!(?force_cand_updates, ?dyn_groups.insts);
trace!(?dyn_groups.insts);
for (dg_uuid, dg_filter) in dyn_groups.insts.iter() { for (dg_uuid, dg_filter) in dyn_groups.insts.iter() {
let dg_filter_valid = dg_filter let dg_filter_valid = dg_filter
@ -308,16 +308,26 @@ impl DynGroup {
let pre_t = pre.entry_match_no_index(&dg_filter_valid); let pre_t = pre.entry_match_no_index(&dg_filter_valid);
let post_t = post.entry_match_no_index(&dg_filter_valid); let post_t = post.entry_match_no_index(&dg_filter_valid);
if pre_t && !post_t { trace!(?post_t, ?force_cand_updates, ?pre_t);
Some(Err(post.get_uuid()))
} else if !pre_t && post_t { // There are some cases where rather than the optimisation to skip
// asserting membership, we need to always assert that membership. Generally
// this occurs in replication where if a candidate was conflicted it can
// trigger a membership delete, but we need to ensure it's still re-added.
if post_t && (force_cand_updates || !pre_t) {
// The entry was added
Some(Ok(post.get_uuid())) Some(Ok(post.get_uuid()))
} else if pre_t && !post_t {
// The entry was deleted
Some(Err(post.get_uuid()))
} else { } else {
None None
} }
}) })
.collect(); .collect();
trace!(?matches);
if !matches.is_empty() { if !matches.is_empty() {
let filt = filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(*dg_uuid))); let filt = filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(*dg_uuid)));
let mut work_set = qs.internal_search_writeable(&filt)?; let mut work_set = qs.internal_search_writeable(&filt)?;
@ -348,6 +358,8 @@ impl DynGroup {
})?; })?;
} }
trace!(?affected_uuids);
Ok(affected_uuids) Ok(affected_uuids)
} }

View file

@ -114,6 +114,7 @@ mod tests {
let uuid = Uuid::new_v4(); let uuid = Uuid::new_v4();
let e: Entry<EntryInit, EntryNew> = entry_init!( let e: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
( (
Attribute::Class, Attribute::Class,
EntryClass::OAuth2ResourceServer.to_value() EntryClass::OAuth2ResourceServer.to_value()
@ -127,10 +128,7 @@ mod tests {
Attribute::DisplayName, Attribute::DisplayName,
Value::new_utf8s("test_resource_server") Value::new_utf8s("test_resource_server")
), ),
( (Attribute::Name, Value::new_iname("test_resource_server")),
Attribute::OAuth2RsName,
Value::new_iname("test_resource_server")
),
( (
Attribute::OAuth2RsOrigin, Attribute::OAuth2RsOrigin,
Value::new_url_s("https://demo.example.com").unwrap() Value::new_url_s("https://demo.example.com").unwrap()
@ -168,6 +166,7 @@ mod tests {
let e: Entry<EntryInit, EntryNew> = entry_init!( let e: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
( (
Attribute::Class, Attribute::Class,
EntryClass::OAuth2ResourceServer.to_value() EntryClass::OAuth2ResourceServer.to_value()
@ -177,10 +176,7 @@ mod tests {
EntryClass::OAuth2ResourceServerBasic.to_value() EntryClass::OAuth2ResourceServerBasic.to_value()
), ),
(Attribute::Uuid, Value::Uuid(uuid)), (Attribute::Uuid, Value::Uuid(uuid)),
( (Attribute::Name, Value::new_iname("test_resource_server")),
Attribute::OAuth2RsName,
Value::new_iname("test_resource_server")
),
( (
Attribute::DisplayName, Attribute::DisplayName,
Value::new_utf8s("test_resource_server") Value::new_utf8s("test_resource_server")

View file

@ -235,12 +235,26 @@ impl Plugin for MemberOf {
qs: &mut QueryServerWriteTransaction, qs: &mut QueryServerWriteTransaction,
pre_cand: &[Arc<EntrySealedCommitted>], pre_cand: &[Arc<EntrySealedCommitted>],
cand: &[EntrySealedCommitted], cand: &[EntrySealedCommitted],
_conflict_uuids: &BTreeSet<Uuid>, conflict_uuids: &BTreeSet<Uuid>,
) -> Result<(), OperationError> { ) -> Result<(), OperationError> {
// If a uuid was in a conflict state, it will be present in the cand/pre_cand set,
// but it *may not* trigger dyn groups as the conflict before and after may satisfy
// the filter as it exists.
//
// In these cases we need to force dynmembers to be reloaded if any conflict occurs
// to ensure that all our memberships are accurate.
let force_dyngroup_cand_update = !conflict_uuids.is_empty();
// IMPORTANT - we need this for now so that dyngroup doesn't error on us, since // IMPORTANT - we need this for now so that dyngroup doesn't error on us, since
// repl is internal and dyngroup has a safety check to prevent external triggers. // repl is internal and dyngroup has a safety check to prevent external triggers.
let ident_internal = Identity::from_internal(); let ident_internal = Identity::from_internal();
Self::post_modify_inner(qs, pre_cand, cand, &ident_internal) Self::post_modify_inner(
qs,
pre_cand,
cand,
&ident_internal,
force_dyngroup_cand_update,
)
} }
#[instrument(level = "debug", name = "memberof_post_modify", skip_all)] #[instrument(level = "debug", name = "memberof_post_modify", skip_all)]
@ -250,7 +264,7 @@ impl Plugin for MemberOf {
cand: &[Entry<EntrySealed, EntryCommitted>], cand: &[Entry<EntrySealed, EntryCommitted>],
me: &ModifyEvent, me: &ModifyEvent,
) -> Result<(), OperationError> { ) -> Result<(), OperationError> {
Self::post_modify_inner(qs, pre_cand, cand, &me.ident) Self::post_modify_inner(qs, pre_cand, cand, &me.ident, false)
} }
#[instrument(level = "debug", name = "memberof_post_batch_modify", skip_all)] #[instrument(level = "debug", name = "memberof_post_batch_modify", skip_all)]
@ -260,7 +274,29 @@ impl Plugin for MemberOf {
cand: &[Entry<EntrySealed, EntryCommitted>], cand: &[Entry<EntrySealed, EntryCommitted>],
me: &BatchModifyEvent, me: &BatchModifyEvent,
) -> Result<(), OperationError> { ) -> Result<(), OperationError> {
Self::post_modify_inner(qs, pre_cand, cand, &me.ident) Self::post_modify_inner(qs, pre_cand, cand, &me.ident, false)
}
#[instrument(level = "debug", name = "memberof_pre_delete", skip_all)]
fn pre_delete(
_qs: &mut QueryServerWriteTransaction,
cand: &mut Vec<EntryInvalidCommitted>,
_de: &DeleteEvent,
) -> Result<(), OperationError> {
// Ensure that when an entry is deleted, that we remove its memberof values,
// and convert direct memberof to recycled direct memberof.
for entry in cand.iter_mut() {
if let Some(direct_mo_vs) = entry.pop_ava(Attribute::DirectMemberOf) {
entry.set_ava_set(Attribute::RecycledDirectMemberOf, direct_mo_vs);
} else {
// Ensure it's empty
entry.purge_ava(Attribute::RecycledDirectMemberOf);
}
entry.purge_ava(Attribute::MemberOf);
}
Ok(())
} }
#[instrument(level = "debug", name = "memberof_post_delete", skip_all)] #[instrument(level = "debug", name = "memberof_post_delete", skip_all)]
@ -368,11 +404,14 @@ impl Plugin for MemberOf {
(None, None) => { (None, None) => {
// Ok // Ok
} }
_ => { (entry_dmo, d_groups) => {
admin_error!( admin_error!(
"MemberOfInvalid directmemberof set and DMO search set differ in size: {}", "MemberOfInvalid directmemberof set and DMO search set differ in size: {}",
e.get_uuid() e.get_uuid()
); );
trace!(?e);
trace!(?entry_dmo);
trace!(?d_groups);
r.push(Err(ConsistencyError::MemberOfInvalid(e.get_id()))); r.push(Err(ConsistencyError::MemberOfInvalid(e.get_id())));
} }
} }
@ -429,8 +468,15 @@ impl MemberOf {
pre_cand: &[Arc<EntrySealedCommitted>], pre_cand: &[Arc<EntrySealedCommitted>],
cand: &[EntrySealedCommitted], cand: &[EntrySealedCommitted],
ident: &Identity, ident: &Identity,
force_dyngroup_cand_update: bool,
) -> Result<(), OperationError> { ) -> Result<(), OperationError> {
let dyngroup_change = super::dyngroup::DynGroup::post_modify(qs, pre_cand, cand, ident)?; let dyngroup_change = super::dyngroup::DynGroup::post_modify(
qs,
pre_cand,
cand,
ident,
force_dyngroup_cand_update,
)?;
// TODO: Limit this to when it's a class, member, mo, dmo change instead. // TODO: Limit this to when it's a class, member, mo, dmo change instead.
let group_affect = cand let group_affect = cand

View file

@ -338,7 +338,8 @@ impl Plugins {
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>, cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
de: &DeleteEvent, de: &DeleteEvent,
) -> Result<(), OperationError> { ) -> Result<(), OperationError> {
protected::Protected::pre_delete(qs, cand, de) protected::Protected::pre_delete(qs, cand, de)?;
memberof::MemberOf::pre_delete(qs, cand, de)
} }
#[instrument(level = "debug", name = "plugins::run_post_delete", skip_all)] #[instrument(level = "debug", name = "plugins::run_post_delete", skip_all)]

View file

@ -397,6 +397,7 @@ mod tests {
Attribute::PrivateCookieKey.to_value() Attribute::PrivateCookieKey.to_value()
), ),
(Attribute::AcpCreateClass, EntryClass::Object.to_value()), (Attribute::AcpCreateClass, EntryClass::Object.to_value()),
(Attribute::AcpCreateClass, EntryClass::Account.to_value()),
(Attribute::AcpCreateClass, EntryClass::Person.to_value()), (Attribute::AcpCreateClass, EntryClass::Person.to_value()),
(Attribute::AcpCreateClass, EntryClass::System.to_value()), (Attribute::AcpCreateClass, EntryClass::System.to_value()),
(Attribute::AcpCreateClass, EntryClass::DomainInfo.to_value()), (Attribute::AcpCreateClass, EntryClass::DomainInfo.to_value()),
@ -438,7 +439,7 @@ mod tests {
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
r#"{ r#"{
"attrs": { "attrs": {
"class": ["person", "system"], "class": ["account", "person", "system"],
"name": ["testperson"], "name": ["testperson"],
"description": ["testperson"], "description": ["testperson"],
"displayname": ["testperson"] "displayname": ["testperson"]
@ -464,7 +465,7 @@ mod tests {
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
r#"{ r#"{
"attrs": { "attrs": {
"class": ["person", "system"], "class": ["account", "person", "system"],
"name": ["testperson"], "name": ["testperson"],
"description": ["testperson"], "description": ["testperson"],
"displayname": ["testperson"] "displayname": ["testperson"]
@ -526,7 +527,7 @@ mod tests {
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str( let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
r#"{ r#"{
"attrs": { "attrs": {
"class": ["person", "system"], "class": ["account", "person", "system"],
"name": ["testperson"], "name": ["testperson"],
"description": ["testperson"], "description": ["testperson"],
"displayname": ["testperson"] "displayname": ["testperson"]

View file

@ -990,15 +990,13 @@ mod tests {
// scope map is also appropriately affected. // scope map is also appropriately affected.
let ea: Entry<EntryInit, EntryNew> = entry_init!( let ea: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
( (
Attribute::Class, Attribute::Class,
EntryClass::OAuth2ResourceServer.to_value() EntryClass::OAuth2ResourceServer.to_value()
), ),
// (Attribute::Class, EntryClass::OAuth2ResourceServerBasic.into()), // (Attribute::Class, EntryClass::OAuth2ResourceServerBasic.into()),
( (Attribute::Name, Value::new_iname("test_resource_server")),
Attribute::OAuth2RsName,
Value::new_iname("test_resource_server")
),
( (
Attribute::DisplayName, Attribute::DisplayName,
Value::new_utf8s("test_resource_server") Value::new_utf8s("test_resource_server")
@ -1037,7 +1035,7 @@ mod tests {
|qs: &mut QueryServerWriteTransaction| { |qs: &mut QueryServerWriteTransaction| {
let cands = qs let cands = qs
.internal_search(filter!(f_eq( .internal_search(filter!(f_eq(
Attribute::OAuth2RsName, Attribute::Name,
PartialValue::new_iname("test_resource_server") PartialValue::new_iname("test_resource_server")
))) )))
.expect("Internal search failure"); .expect("Internal search failure");
@ -1080,15 +1078,13 @@ mod tests {
let e2 = entry_init!( let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
( (
Attribute::Class, Attribute::Class,
EntryClass::OAuth2ResourceServer.to_value() EntryClass::OAuth2ResourceServer.to_value()
), ),
(Attribute::Uuid, Value::Uuid(rs_uuid)), (Attribute::Uuid, Value::Uuid(rs_uuid)),
( (Attribute::Name, Value::new_iname("test_resource_server")),
Attribute::OAuth2RsName,
Value::new_iname("test_resource_server")
),
( (
Attribute::DisplayName, Attribute::DisplayName,
Value::new_utf8s("test_resource_server") Value::new_utf8s("test_resource_server")
@ -1129,7 +1125,7 @@ mod tests {
Value::Oauth2Session( Value::Oauth2Session(
session_id, session_id,
Oauth2Session { Oauth2Session {
parent: parent_id, parent: Some(parent_id),
// Note we set the exp to None so we are not removing based on exp // Note we set the exp to None so we are not removing based on exp
state: SessionState::NeverExpires, state: SessionState::NeverExpires,
issued_at, issued_at,
@ -1309,6 +1305,7 @@ mod tests {
fn test_delete_remove_reference_oauth2_claim_map() { fn test_delete_remove_reference_oauth2_claim_map() {
let ea: Entry<EntryInit, EntryNew> = entry_init!( let ea: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
( (
Attribute::Class, Attribute::Class,
EntryClass::OAuth2ResourceServer.to_value() EntryClass::OAuth2ResourceServer.to_value()
@ -1317,10 +1314,7 @@ mod tests {
Attribute::Class, Attribute::Class,
EntryClass::OAuth2ResourceServerPublic.to_value() EntryClass::OAuth2ResourceServerPublic.to_value()
), ),
( (Attribute::Name, Value::new_iname("test_resource_server")),
Attribute::OAuth2RsName,
Value::new_iname("test_resource_server")
),
( (
Attribute::DisplayName, Attribute::DisplayName,
Value::new_utf8s("test_resource_server") Value::new_utf8s("test_resource_server")
@ -1366,7 +1360,7 @@ mod tests {
|qs: &mut QueryServerWriteTransaction| { |qs: &mut QueryServerWriteTransaction| {
let cands = qs let cands = qs
.internal_search(filter!(f_eq( .internal_search(filter!(f_eq(
Attribute::OAuth2RsName, Attribute::Name,
PartialValue::new_iname("test_resource_server") PartialValue::new_iname("test_resource_server")
))) )))
.expect("Internal search failure"); .expect("Internal search failure");

View file

@ -131,13 +131,20 @@ impl SessionConsistency {
_ => { _ => {
// Okay, now check the issued / grace time for parent enforcement. // Okay, now check the issued / grace time for parent enforcement.
if sessions.map(|session_map| { if sessions.map(|session_map| {
if let Some(parent_session) = session_map.get(&session.parent) { if let Some(parent_session_id) = session.parent.as_ref() {
// A parent session id exists - validate it exists in the account.
if let Some(parent_session) = session_map.get(parent_session_id) {
// Only match non-revoked sessions // Only match non-revoked sessions
!matches!(parent_session.state, SessionState::RevokedAt(_)) !matches!(parent_session.state, SessionState::RevokedAt(_))
} else { } else {
// not found // not found
false false
} }
} else {
// The session specifically has no parent session and so is
// not bounded by it's presence.
true
}
}).unwrap_or(false) { }).unwrap_or(false) {
// The parent exists and is still valid, go ahead // The parent exists and is still valid, go ahead
debug!("Parent session remains valid."); debug!("Parent session remains valid.");
@ -145,7 +152,7 @@ impl SessionConsistency {
} else { } else {
// Can't find the parent. Are we within grace window // Can't find the parent. Are we within grace window
if session.issued_at + GRACE_WINDOW <= curtime_odt { if session.issued_at + GRACE_WINDOW <= curtime_odt {
info!(%o2_session_id, parent_id = %session.parent, "Removing orphaned oauth2 session"); info!(%o2_session_id, parent_id = ?session.parent, "Removing orphaned oauth2 session");
Some(PartialValue::Refer(*o2_session_id)) Some(PartialValue::Refer(*o2_session_id))
} else { } else {
// Grace window is still in effect // Grace window is still in effect
@ -330,6 +337,7 @@ mod tests {
let e2 = entry_init!( let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
( (
Attribute::Class, Attribute::Class,
EntryClass::OAuth2ResourceServer.to_value() EntryClass::OAuth2ResourceServer.to_value()
@ -339,10 +347,7 @@ mod tests {
EntryClass::OAuth2ResourceServerBasic.to_value() EntryClass::OAuth2ResourceServerBasic.to_value()
), ),
(Attribute::Uuid, Value::Uuid(rs_uuid)), (Attribute::Uuid, Value::Uuid(rs_uuid)),
( (Attribute::Name, Value::new_iname("test_resource_server")),
Attribute::OAuth2RsName,
Value::new_iname("test_resource_server")
),
( (
Attribute::DisplayName, Attribute::DisplayName,
Value::new_utf8s("test_resource_server") Value::new_utf8s("test_resource_server")
@ -381,7 +386,7 @@ mod tests {
Value::Oauth2Session( Value::Oauth2Session(
session_id, session_id,
Oauth2Session { Oauth2Session {
parent: parent_id, parent: Some(parent_id),
// Set to the exp window. // Set to the exp window.
state, state,
issued_at, issued_at,
@ -505,6 +510,7 @@ mod tests {
let e2 = entry_init!( let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
( (
Attribute::Class, Attribute::Class,
EntryClass::OAuth2ResourceServer.to_value() EntryClass::OAuth2ResourceServer.to_value()
@ -514,10 +520,7 @@ mod tests {
EntryClass::OAuth2ResourceServerBasic.to_value() EntryClass::OAuth2ResourceServerBasic.to_value()
), ),
(Attribute::Uuid, Value::Uuid(rs_uuid)), (Attribute::Uuid, Value::Uuid(rs_uuid)),
( (Attribute::Name, Value::new_iname("test_resource_server")),
Attribute::OAuth2RsName,
Value::new_iname("test_resource_server")
),
( (
Attribute::DisplayName, Attribute::DisplayName,
Value::new_utf8s("test_resource_server") Value::new_utf8s("test_resource_server")
@ -555,7 +558,7 @@ mod tests {
Value::Oauth2Session( Value::Oauth2Session(
session_id, session_id,
Oauth2Session { Oauth2Session {
parent: parent_id, parent: Some(parent_id),
// Note we set the exp to None so we are not removing based on exp // Note we set the exp to None so we are not removing based on exp
state: SessionState::NeverExpires, state: SessionState::NeverExpires,
issued_at, issued_at,
@ -673,6 +676,7 @@ mod tests {
let e2 = entry_init!( let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
( (
Attribute::Class, Attribute::Class,
EntryClass::OAuth2ResourceServer.to_value() EntryClass::OAuth2ResourceServer.to_value()
@ -682,10 +686,7 @@ mod tests {
EntryClass::OAuth2ResourceServerBasic.to_value() EntryClass::OAuth2ResourceServerBasic.to_value()
), ),
(Attribute::Uuid, Value::Uuid(rs_uuid)), (Attribute::Uuid, Value::Uuid(rs_uuid)),
( (Attribute::Name, Value::new_iname("test_resource_server")),
Attribute::OAuth2RsName,
Value::new_iname("test_resource_server")
),
( (
Attribute::DisplayName, Attribute::DisplayName,
Value::new_utf8s("test_resource_server") Value::new_utf8s("test_resource_server")
@ -716,7 +717,7 @@ mod tests {
let session = Value::Oauth2Session( let session = Value::Oauth2Session(
session_id, session_id,
Oauth2Session { Oauth2Session {
parent, parent: Some(parent),
// Note we set the exp to None so we are asserting the removal is due to the lack // Note we set the exp to None so we are asserting the removal is due to the lack
// of the parent session. // of the parent session.
state: SessionState::NeverExpires, state: SessionState::NeverExpires,

View file

@ -136,6 +136,7 @@ mod tests {
assert!(server_txn assert!(server_txn
.internal_create(vec![entry_init!( .internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("tobias")), (Attribute::Name, Value::new_iname("tobias")),
(Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Uuid, Value::Uuid(t_uuid)),
@ -154,6 +155,7 @@ mod tests {
assert!(server_txn assert!(server_txn
.internal_create(vec![entry_init!( .internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("newname")), (Attribute::Name, Value::new_iname("newname")),
(Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Uuid, Value::Uuid(t_uuid)),
@ -181,6 +183,7 @@ mod tests {
assert!(server_txn assert!(server_txn
.internal_create(vec![entry_init!( .internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("newname")), (Attribute::Name, Value::new_iname("newname")),
(Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Uuid, Value::Uuid(t_uuid)),

View file

@ -184,10 +184,18 @@ impl<'a> QueryServerWriteTransaction<'a> {
// //
let (cand, pre_cand): (Vec<_>, Vec<_>) = all_updates_valid let (cand, pre_cand): (Vec<_>, Vec<_>) = all_updates_valid
.into_iter() .into_iter()
// We previously excluded this to avoid doing unnecesary work on entries that
// were moving to a conflict state, and the survivor was staying "as is" on this
// node. However, this gets messy with dyngroups and memberof, where on a conflict
// the memberships are deleted across the replication boundary. In these cases
// we need dyngroups to see the valid entries, even if they are "identical to before"
// to re-assert all their memberships are valid.
/*
.filter(|(cand, _)| { .filter(|(cand, _)| {
// Exclude anything that is conflicted as a result of the conflict plugins. // Exclude anything that is conflicted as a result of the conflict plugins.
!conflict_uuids.contains(&cand.get_uuid()) !conflict_uuids.contains(&cand.get_uuid())
}) })
*/
.unzip(); .unzip();
// We don't need to process conflict_creates here, since they are all conflicting // We don't need to process conflict_creates here, since they are all conflicting

View file

@ -215,9 +215,9 @@ impl EntryChangeState {
} }
#[cfg(test)] #[cfg(test)]
pub(crate) fn get_attr_cid(&self, attr: &Attribute) -> Option<Cid> { pub(crate) fn get_attr_cid(&self, attr: &Attribute) -> Option<&Cid> {
match &self.st { match &self.st {
State::Live { at: _, changes } => changes.get(attr.as_ref()).map(|cid| cid.clone()), State::Live { at: _, changes } => changes.get(attr.as_ref()),
State::Tombstone { at: _ } => None, State::Tombstone { at: _ } => None,
} }
} }

View file

@ -9,6 +9,7 @@ use crate::schema::{SchemaReadTransaction, SchemaTransaction};
use crate::valueset; use crate::valueset;
use base64urlsafedata::Base64UrlSafeData; use base64urlsafedata::Base64UrlSafeData;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use webauthn_rs::prelude::{ use webauthn_rs::prelude::{
@ -261,10 +262,11 @@ pub struct ReplOauthClaimMapV1 {
pub values: BTreeMap<Uuid, BTreeSet<String>>, pub values: BTreeMap<Uuid, BTreeSet<String>>,
} }
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct ReplOauth2SessionV1 { pub struct ReplOauth2SessionV1 {
pub refer: Uuid, pub refer: Uuid,
pub parent: Uuid, pub parent: Option<Uuid>,
pub state: ReplSessionStateV1, pub state: ReplSessionStateV1,
// pub expiry: Option<String>, // pub expiry: Option<String>,
pub issued_at: String, pub issued_at: String,

View file

@ -148,9 +148,13 @@ impl<'a> QueryServerReadTransaction<'a> {
} }
}; };
debug!(?ranges, "these ranges will be supplied"); debug!("these ranges will be supplied");
debug!(supply_ranges = ?ranges);
debug!(consumer_ranges = ?ctx_ranges);
debug!(supplier_ranges = ?our_ranges);
if ranges.is_empty() { if ranges.is_empty() {
debug!("No Changes Available");
return Ok(ReplIncrementalContext::NoChangesAvailable); return Ok(ReplIncrementalContext::NoChangesAvailable);
} }

View file

@ -250,6 +250,7 @@ async fn test_repl_increment_basic_entry_add(server_a: &QueryServer, server_b: &
assert!(server_b_txn assert!(server_b_txn
.internal_create(vec![entry_init!( .internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Uuid, Value::Uuid(t_uuid)),
@ -353,6 +354,7 @@ async fn test_repl_increment_basic_entry_recycle(server_a: &QueryServer, server_
assert!(server_b_txn assert!(server_b_txn
.internal_create(vec![entry_init!( .internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Uuid, Value::Uuid(t_uuid)),
@ -411,6 +413,7 @@ async fn test_repl_increment_basic_entry_tombstone(server_a: &QueryServer, serve
assert!(server_b_txn assert!(server_b_txn
.internal_create(vec![entry_init!( .internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Uuid, Value::Uuid(t_uuid)),
@ -481,6 +484,7 @@ async fn test_repl_increment_consumer_lagging_tombstone(
assert!(server_b_txn assert!(server_b_txn
.internal_create(vec![entry_init!( .internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Uuid, Value::Uuid(t_uuid)),
@ -584,6 +588,7 @@ async fn test_repl_increment_basic_bidirectional_write(
assert!(server_b_txn assert!(server_b_txn
.internal_create(vec![entry_init!( .internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Uuid, Value::Uuid(t_uuid)),
@ -664,6 +669,7 @@ async fn test_repl_increment_basic_deleted_attr(server_a: &QueryServer, server_b
assert!(server_a_txn assert!(server_a_txn
.internal_create(vec![entry_init!( .internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Uuid, Value::Uuid(t_uuid)),
@ -731,6 +737,7 @@ async fn test_repl_increment_simultaneous_bidirectional_write(
assert!(server_b_txn assert!(server_b_txn
.internal_create(vec![entry_init!( .internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Uuid, Value::Uuid(t_uuid)),
@ -842,6 +849,7 @@ async fn test_repl_increment_basic_bidirectional_lifecycle(
assert!(server_b_txn assert!(server_b_txn
.internal_create(vec![entry_init!( .internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Uuid, Value::Uuid(t_uuid)),
@ -983,6 +991,7 @@ async fn test_repl_increment_basic_bidirectional_recycle(
assert!(server_b_txn assert!(server_b_txn
.internal_create(vec![entry_init!( .internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Uuid, Value::Uuid(t_uuid)),
@ -1109,6 +1118,7 @@ async fn test_repl_increment_basic_bidirectional_tombstone(
assert!(server_b_txn assert!(server_b_txn
.internal_create(vec![entry_init!( .internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Uuid, Value::Uuid(t_uuid)),
@ -1213,6 +1223,7 @@ async fn test_repl_increment_creation_uuid_conflict(
let t_uuid = Uuid::new_v4(); let t_uuid = Uuid::new_v4();
let e_init = entry_init!( let e_init = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Uuid, Value::Uuid(t_uuid)),
@ -1244,6 +1255,19 @@ async fn test_repl_increment_creation_uuid_conflict(
.internal_search_all_uuid(t_uuid) .internal_search_all_uuid(t_uuid)
.expect("Unable to access entry."); .expect("Unable to access entry.");
let e1_acc = server_a_txn
.internal_search_all_uuid(UUID_IDM_ALL_ACCOUNTS)
.expect("Unable to access new entry.");
let e2_acc = server_b_txn
.internal_search_all_uuid(UUID_IDM_ALL_ACCOUNTS)
.expect("Unable to access entry.");
trace!("TESTMARKER 0");
trace!(?e1);
trace!(?e2);
trace!(?e1_acc);
trace!(?e2_acc);
trace!("{:?}", e1.get_last_changed()); trace!("{:?}", e1.get_last_changed());
trace!("{:?}", e2.get_last_changed()); trace!("{:?}", e2.get_last_changed());
// e2 from b will be smaller as it's the older entry. // e2 from b will be smaller as it's the older entry.
@ -1277,6 +1301,19 @@ async fn test_repl_increment_creation_uuid_conflict(
.internal_search_all_uuid(t_uuid) .internal_search_all_uuid(t_uuid)
.expect("Unable to access entry."); .expect("Unable to access entry.");
let e1_acc = server_a_txn
.internal_search_all_uuid(UUID_IDM_ALL_ACCOUNTS)
.expect("Unable to access new entry.");
let e2_acc = server_b_txn
.internal_search_all_uuid(UUID_IDM_ALL_ACCOUNTS)
.expect("Unable to access entry.");
trace!("TESTMARKER 1");
trace!(?e1);
trace!(?e2);
trace!(?e1_acc);
trace!(?e2_acc);
assert!(e1.get_last_changed() == e2.get_last_changed()); assert!(e1.get_last_changed() == e2.get_last_changed());
let cnf_a = server_a_txn let cnf_a = server_a_txn
@ -1292,6 +1329,10 @@ async fn test_repl_increment_creation_uuid_conflict(
.expect("Unable to conflict entries."); .expect("Unable to conflict entries.");
assert!(cnf_b.is_empty()); assert!(cnf_b.is_empty());
trace!("TESTMARKER 2");
trace!(?cnf_a);
trace!(?cnf_b);
server_a_txn.commit().expect("Failed to commit"); server_a_txn.commit().expect("Failed to commit");
drop(server_b_txn); drop(server_b_txn);
@ -1319,8 +1360,32 @@ async fn test_repl_increment_creation_uuid_conflict(
.pop() .pop()
.expect("No conflict entries present"); .expect("No conflict entries present");
trace!("TESTMARKER 3");
trace!(?cnf_a);
trace!(?cnf_b);
assert!(cnf_a.get_last_changed() == cnf_b.get_last_changed()); assert!(cnf_a.get_last_changed() == cnf_b.get_last_changed());
let e1 = server_a_txn
.internal_search_all_uuid(t_uuid)
.expect("Unable to access new entry.");
let e2 = server_b_txn
.internal_search_all_uuid(t_uuid)
.expect("Unable to access entry.");
let e1_acc = server_a_txn
.internal_search_all_uuid(UUID_IDM_ALL_ACCOUNTS)
.expect("Unable to access new entry.");
let e2_acc = server_b_txn
.internal_search_all_uuid(UUID_IDM_ALL_ACCOUNTS)
.expect("Unable to access entry.");
trace!("TESTMARKER 4");
trace!(?e1);
trace!(?e2);
trace!(?e1_acc);
trace!(?e2_acc);
server_b_txn.commit().expect("Failed to commit"); server_b_txn.commit().expect("Failed to commit");
drop(server_a_txn); drop(server_a_txn);
} }
@ -1344,6 +1409,7 @@ async fn test_repl_increment_create_tombstone_uuid_conflict(
let t_uuid = Uuid::new_v4(); let t_uuid = Uuid::new_v4();
let e_init = entry_init!( let e_init = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Uuid, Value::Uuid(t_uuid)),
@ -1437,6 +1503,7 @@ async fn test_repl_increment_create_tombstone_conflict(
let t_uuid = Uuid::new_v4(); let t_uuid = Uuid::new_v4();
let e_init = entry_init!( let e_init = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Uuid, Value::Uuid(t_uuid)),
@ -1533,6 +1600,7 @@ async fn test_repl_increment_schema_conflict(server_a: &QueryServer, server_b: &
assert!(server_b_txn assert!(server_b_txn
.internal_create(vec![entry_init!( .internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Uuid, Value::Uuid(t_uuid)),
@ -1570,8 +1638,10 @@ async fn test_repl_increment_schema_conflict(server_a: &QueryServer, server_b: &
let mut server_b_txn = server_b.write(ct).await; let mut server_b_txn = server_b.write(ct).await;
let modlist = ModifyList::new_list(vec![ let modlist = ModifyList::new_list(vec![
Modify::Removed(Attribute::Class.into(), EntryClass::Person.into()), Modify::Removed(Attribute::Class.into(), EntryClass::Person.into()),
Modify::Removed(Attribute::Class.into(), EntryClass::Account.into()),
Modify::Present(Attribute::Class.into(), EntryClass::Group.into()), Modify::Present(Attribute::Class.into(), EntryClass::Group.into()),
Modify::Purged(Attribute::IdVerificationEcKey.into()), Modify::Purged(Attribute::IdVerificationEcKey.into()),
Modify::Purged(Attribute::NameHistory.into()),
Modify::Purged(Attribute::DisplayName.into()), Modify::Purged(Attribute::DisplayName.into()),
]); ]);
assert!(server_b_txn.internal_modify_uuid(t_uuid, &modlist).is_ok()); assert!(server_b_txn.internal_modify_uuid(t_uuid, &modlist).is_ok());
@ -1650,6 +1720,7 @@ async fn test_repl_increment_consumer_lagging_attributes(
assert!(server_b_txn assert!(server_b_txn
.internal_create(vec![entry_init!( .internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Uuid, Value::Uuid(t_uuid)),
@ -1775,6 +1846,7 @@ async fn test_repl_increment_consumer_ruv_trim_past_valid(
assert!(server_b_txn assert!(server_b_txn
.internal_create(vec![entry_init!( .internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Uuid, Value::Uuid(t_uuid)),
@ -1905,6 +1977,7 @@ async fn test_repl_increment_consumer_ruv_trim_idle_servers(
assert!(server_b_txn assert!(server_b_txn
.internal_create(vec![entry_init!( .internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Uuid, Value::Uuid(t_uuid)),

View file

@ -1433,6 +1433,25 @@ impl<'a> SchemaWriteTransaction<'a> {
syntax: SyntaxType::ReferenceUuid, syntax: SyntaxType::ReferenceUuid,
}, },
); );
self.attributes.insert(
Attribute::RecycledDirectMemberOf.into(),
SchemaAttribute {
name: Attribute::RecycledDirectMemberOf.into(),
uuid: UUID_SCHEMA_ATTR_RECYCLEDDIRECTMEMBEROF,
description: String::from("recycled reverse direct group membership of the object to assist in revive operations."),
multivalue: true,
unique: false,
phantom: false,
sync_allowed: false,
// Unlike DMO this must be replicated so that on a recycle event, these groups
// "at delete" are replicated to partners. This avoids us having to replicate
// DMO which is very costly, while still retaining our ability to revive entries
// and their group memberships as a best effort.
replicated: true,
index: vec![],
syntax: SyntaxType::ReferenceUuid,
},
);
self.attributes.insert( self.attributes.insert(
Attribute::Member.into(), Attribute::Member.into(),
SchemaAttribute { SchemaAttribute {
@ -1974,6 +1993,7 @@ impl<'a> SchemaWriteTransaction<'a> {
name: EntryClass::Recycled.into(), name: EntryClass::Recycled.into(),
uuid: UUID_SCHEMA_CLASS_RECYCLED, uuid: UUID_SCHEMA_CLASS_RECYCLED,
description: String::from("An object that has been deleted, but still recoverable via the revive operation. Recycled objects are not modifiable, only revivable."), description: String::from("An object that has been deleted, but still recoverable via the revive operation. Recycled objects are not modifiable, only revivable."),
systemmay: vec![Attribute::RecycledDirectMemberOf.into()],
.. Default::default() .. Default::default()
}, },
); );

View file

@ -2719,10 +2719,7 @@ mod tests {
EntryClass::OAuth2ResourceServerBasic.to_value() EntryClass::OAuth2ResourceServerBasic.to_value()
), ),
(Attribute::Uuid, Value::Uuid(rs_uuid)), (Attribute::Uuid, Value::Uuid(rs_uuid)),
( (Attribute::Name, Value::new_iname("test_resource_server")),
Attribute::OAuth2RsName,
Value::new_iname("test_resource_server")
),
( (
Attribute::DisplayName, Attribute::DisplayName,
Value::new_utf8s("test_resource_server") Value::new_utf8s("test_resource_server")
@ -2764,10 +2761,7 @@ mod tests {
EntryClass::OAuth2ResourceServerBasic.to_value() EntryClass::OAuth2ResourceServerBasic.to_value()
), ),
(Attribute::Uuid, Value::Uuid(rs_uuid)), (Attribute::Uuid, Value::Uuid(rs_uuid)),
( (Attribute::Name, Value::new_iname("test_resource_server")),
Attribute::OAuth2RsName,
Value::new_iname("test_resource_server")
),
( (
Attribute::DisplayName, Attribute::DisplayName,
Value::new_utf8s("test_resource_server") Value::new_utf8s("test_resource_server")
@ -2790,10 +2784,7 @@ mod tests {
EntryClass::OAuth2ResourceServerBasic.to_value() EntryClass::OAuth2ResourceServerBasic.to_value()
), ),
(Attribute::Uuid, Value::Uuid(Uuid::new_v4())), (Attribute::Uuid, Value::Uuid(Uuid::new_v4())),
( (Attribute::Name, Value::new_iname("second_resource_server")),
Attribute::OAuth2RsName,
Value::new_iname("second_resource_server")
),
( (
Attribute::DisplayName, Attribute::DisplayName,
Value::new_utf8s("second_resource_server") Value::new_utf8s("second_resource_server")
@ -2832,14 +2823,14 @@ mod tests {
let se_a = SearchEvent::new_impersonate_entry( let se_a = SearchEvent::new_impersonate_entry(
E_TEST_ACCOUNT_1.clone(), E_TEST_ACCOUNT_1.clone(),
filter_all!(f_pres(Attribute::OAuth2RsName)), filter_all!(f_pres(Attribute::Name)),
); );
let ex_a = vec![Arc::new(ev1)]; let ex_a = vec![Arc::new(ev1)];
let ex_a_reduced = vec![ev1_reduced]; let ex_a_reduced = vec![ev1_reduced];
let se_b = SearchEvent::new_impersonate_entry( let se_b = SearchEvent::new_impersonate_entry(
E_TEST_ACCOUNT_2.clone(), E_TEST_ACCOUNT_2.clone(),
filter_all!(f_pres(Attribute::OAuth2RsName)), filter_all!(f_pres(Attribute::Name)),
); );
let ex_b = vec![]; let ex_b = vec![];

View file

@ -191,7 +191,7 @@ fn search_oauth2_filter_entry<'a>(
Attribute::Class.as_ref(), Attribute::Class.as_ref(),
Attribute::DisplayName.as_ref(), Attribute::DisplayName.as_ref(),
Attribute::Uuid.as_ref(), Attribute::Uuid.as_ref(),
Attribute::OAuth2RsName.as_ref(), Attribute::Name.as_ref(),
Attribute::OAuth2RsOrigin.as_ref(), Attribute::OAuth2RsOrigin.as_ref(),
Attribute::OAuth2RsOriginLanding.as_ref(), Attribute::OAuth2RsOriginLanding.as_ref(),
Attribute::Image.as_ref() Attribute::Image.as_ref()

View file

@ -204,6 +204,7 @@ mod tests {
let e1 = entry_init!( let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
( (
@ -216,6 +217,7 @@ mod tests {
let e2 = entry_init!( let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson2")), (Attribute::Name, Value::new_iname("testperson2")),
( (
@ -228,6 +230,7 @@ mod tests {
let e3 = entry_init!( let e3 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson3")), (Attribute::Name, Value::new_iname("testperson3")),
( (

View file

@ -739,6 +739,84 @@ impl<'a> QueryServerWriteTransaction<'a> {
}) })
} }
#[instrument(level = "info", skip_all)]
/// Migrations for Oauth to move rs name from a dedicated type to name
/// and to allow oauth2 sessions on resource servers for client credentials
/// grants. Accounts, persons and service accounts have some attributes
/// relocated to allow oauth2 rs to become accounts.
pub fn migrate_domain_4_to_5(&mut self) -> Result<(), OperationError> {
let idm_schema_classes = [
SCHEMA_CLASS_PERSON_DL5.clone().into(),
SCHEMA_CLASS_ACCOUNT_DL5.clone().into(),
SCHEMA_CLASS_SERVICE_ACCOUNT_DL5.clone().into(),
SCHEMA_CLASS_OAUTH2_RS_DL5.clone().into(),
SCHEMA_CLASS_OAUTH2_RS_BASIC_DL5.clone().into(),
IDM_ACP_OAUTH2_MANAGE_DL5.clone().into(),
];
idm_schema_classes
.into_iter()
.try_for_each(|entry| self.internal_migrate_or_create(entry))
.map_err(|err| {
error!(?err, "migrate_domain_4_to_5 -> Error");
err
})?;
// Reload mid txn so that the next modification works.
self.force_schema_reload();
self.reload()?;
// Now we remove attributes from service accounts that have been unable to be set
// via a user interface for more than a year.
let filter = filter!(f_and!([
f_eq(Attribute::Class, EntryClass::Account.into()),
f_eq(Attribute::Class, EntryClass::ServiceAccount.into()),
]));
let modlist = ModifyList::new_list(vec![
Modify::Purged(Attribute::PassKeys.into()),
Modify::Purged(Attribute::AttestedPasskeys.into()),
Modify::Purged(Attribute::CredentialUpdateIntentToken.into()),
Modify::Purged(Attribute::RadiusSecret.into()),
]);
self.internal_modify(&filter, &modlist)?;
// Now move all oauth2 rs name.
let filter = filter!(f_eq(
Attribute::Class,
EntryClass::OAuth2ResourceServer.into()
));
let pre_candidates = self.internal_search(filter).map_err(|err| {
admin_error!(?err, "migrate_domain_4_to_5 internal search failure");
err
})?;
let modset: Vec<_> = pre_candidates
.into_iter()
.filter_map(|ent| {
ent.get_ava_single_iname(Attribute::OAuth2RsName)
.map(|rs_name| {
let modlist = vec![
Modify::Present(Attribute::Class.into(), EntryClass::Account.into()),
Modify::Present(Attribute::Name.into(), Value::new_iname(rs_name)),
m_purge(Attribute::OAuth2RsName),
];
(ent.get_uuid(), ModifyList::new_list(modlist))
})
})
.collect();
// If there is nothing, we don't need to do anything.
if modset.is_empty() {
admin_info!("migrate_domain_4_to_5 no entries to migrate, complete");
return Ok(());
}
// Apply the batch mod.
self.internal_batch_modify(modset.into_iter())
}
#[instrument(level = "info", skip_all)] #[instrument(level = "info", skip_all)]
pub fn initialise_schema_core(&mut self) -> Result<(), OperationError> { pub fn initialise_schema_core(&mut self) -> Result<(), OperationError> {
admin_debug!("initialise_schema_core -> start ..."); admin_debug!("initialise_schema_core -> start ...");

View file

@ -1159,9 +1159,9 @@ impl QueryServer {
let d_info = Arc::new(CowCell::new(DomainInfo { let d_info = Arc::new(CowCell::new(DomainInfo {
d_uuid, d_uuid,
// Start with our minimum supported level. // Start with our level as zero.
// This will be reloaded from the DB shortly :) // This will be reloaded from the DB shortly :)
d_vers: DOMAIN_MIN_LEVEL, d_vers: DOMAIN_LEVEL_0,
d_name: domain_name.clone(), d_name: domain_name.clone(),
// we set the domain_display_name to the configuration file's domain_name // we set the domain_display_name to the configuration file's domain_name
// here because the database is not started, so we cannot pull it from there. // here because the database is not started, so we cannot pull it from there.
@ -1630,6 +1630,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
self.migrate_domain_3_to_4()?; self.migrate_domain_3_to_4()?;
} }
if previous_version <= DOMAIN_LEVEL_4 && domain_info_version >= DOMAIN_LEVEL_5 {
self.migrate_domain_4_to_5()?;
}
Ok(()) Ok(())
} }
@ -1853,6 +1857,7 @@ mod tests {
assert!(server_txn assert!(server_txn
.internal_create(vec![entry_init!( .internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(t_uuid)), (Attribute::Uuid, Value::Uuid(t_uuid)),
@ -1983,6 +1988,7 @@ mod tests {
let mut server_txn = server.write(duration_from_epoch_now()).await; let mut server_txn = server.write(duration_from_epoch_now()).await;
let e1 = entry_init!( let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
( (

View file

@ -209,9 +209,21 @@ impl<'a> QueryServerWriteTransaction<'a> {
if !self.changed_oauth2 { if !self.changed_oauth2 {
self.changed_oauth2 = norm_cand self.changed_oauth2 = norm_cand
.iter() .iter()
.chain(pre_candidates.iter().map(|e| e.as_ref())) .zip(pre_candidates.iter().map(|e| e.as_ref()))
.any(|e| { .any(|(post, pre)| {
e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into()) // This is in the modify path only - because sessions can update the RS
// this can trigger reloads of all the oauth2 clients. That would make
// client credentials grant pretty expensive in these cases. To avoid this
// we check if "anything else" beside the oauth2session changed in this
// txn.
(post.attribute_equality(
Attribute::Class,
&EntryClass::OAuth2ResourceServer.into(),
) || pre.attribute_equality(
Attribute::Class,
&EntryClass::OAuth2ResourceServer.into(),
)) && post
.entry_changed_excluding_attribute(Attribute::OAuth2Session, &self.cid)
}); });
} }
if !self.changed_domain { if !self.changed_domain {
@ -511,6 +523,7 @@ mod tests {
let e1 = entry_init!( let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
( (
@ -523,6 +536,7 @@ mod tests {
let e2 = entry_init!( let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson2")), (Attribute::Name, Value::new_iname("testperson2")),
( (
@ -671,6 +685,7 @@ mod tests {
let e1 = entry_init!( let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
( (

View file

@ -148,7 +148,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
// Get this entries uuid. // Get this entries uuid.
let u: Uuid = e.get_uuid(); let u: Uuid = e.get_uuid();
if let Some(riter) = e.get_ava_as_refuuid(Attribute::DirectMemberOf) { if let Some(riter) = e.get_ava_as_refuuid(Attribute::RecycledDirectMemberOf) {
for g_uuid in riter { for g_uuid in riter {
dm_mods dm_mods
.entry(g_uuid) .entry(g_uuid)
@ -291,6 +291,7 @@ mod tests {
// Create some recycled objects // Create some recycled objects
let e1 = entry_init!( let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
( (
@ -303,6 +304,7 @@ mod tests {
let e2 = entry_init!( let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson2")), (Attribute::Name, Value::new_iname("testperson2")),
( (
@ -397,6 +399,7 @@ mod tests {
let e1 = entry_init!( let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
( (
@ -532,6 +535,7 @@ mod tests {
// First, create an entry, then push it through the lifecycle. // First, create an entry, then push it through the lifecycle.
let e_ts = entry_init!( let e_ts = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")), (Attribute::Name, Value::new_iname("testperson1")),
( (
@ -610,6 +614,7 @@ mod tests {
fn create_user(name: &str, uuid: &str) -> Entry<EntryInit, EntryNew> { fn create_user(name: &str, uuid: &str) -> Entry<EntryInit, EntryNew> {
entry_init!( entry_init!(
(Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()), (Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname(name)), (Attribute::Name, Value::new_iname(name)),
( (
@ -639,7 +644,7 @@ mod tests {
} }
fn check_entry_has_mo(qs: &mut QueryServerWriteTransaction, name: &str, mo: &str) -> bool { fn check_entry_has_mo(qs: &mut QueryServerWriteTransaction, name: &str, mo: &str) -> bool {
let e = qs let entry = qs
.internal_search(filter!(f_eq( .internal_search(filter!(f_eq(
Attribute::Name, Attribute::Name,
PartialValue::new_iname(name) PartialValue::new_iname(name)
@ -648,7 +653,9 @@ mod tests {
.pop() .pop()
.unwrap(); .unwrap();
e.attribute_equality(Attribute::MemberOf, &PartialValue::new_refer_s(mo).unwrap()) trace!(?entry);
entry.attribute_equality(Attribute::MemberOf, &PartialValue::new_refer_s(mo).unwrap())
} }
#[qs_test] #[qs_test]

View file

@ -1030,8 +1030,7 @@ impl From<OauthClaimMapJoin> for DbValueOauthClaimMapJoinV1 {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Oauth2Session { pub struct Oauth2Session {
pub parent: Uuid, pub parent: Option<Uuid>,
// pub expiry: Option<OffsetDateTime>,
pub state: SessionState, pub state: SessionState,
pub issued_at: OffsetDateTime, pub issued_at: OffsetDateTime,
pub rs_uuid: Uuid, pub rs_uuid: Uuid,

View file

@ -779,6 +779,8 @@ impl ValueSetOauth2Session {
.map(SessionState::ExpiresAt) .map(SessionState::ExpiresAt)
.unwrap_or(SessionState::NeverExpires); .unwrap_or(SessionState::NeverExpires);
let parent = Some(parent);
// Insert to the rs_filter. // Insert to the rs_filter.
rs_filter |= rs_uuid.as_u128(); rs_filter |= rs_uuid.as_u128();
Some(( Some((
@ -833,6 +835,8 @@ impl ValueSetOauth2Session {
rs_filter |= rs_uuid.as_u128(); rs_filter |= rs_uuid.as_u128();
let parent = Some(parent);
Some(( Some((
refer, refer,
Oauth2Session { Oauth2Session {
@ -842,7 +846,59 @@ impl ValueSetOauth2Session {
rs_uuid, rs_uuid,
}, },
)) ))
} // End V2
DbValueOauth2Session::V3 {
refer,
parent,
state,
issued_at,
rs_uuid,
} => {
// Convert things.
let issued_at = OffsetDateTime::parse(&issued_at, &Rfc3339)
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
.map_err(|e| {
admin_error!(
?e,
"Invalidating session {} due to invalid issued_at timestamp",
refer
)
})
.ok()?;
let state = match state {
DbValueSessionStateV1::ExpiresAt(e_inner) => {
OffsetDateTime::parse(&e_inner, &Rfc3339)
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
.map(SessionState::ExpiresAt)
.map_err(|e| {
admin_error!(
?e,
"Invalidating session {} due to invalid expiry timestamp",
refer
)
})
.ok()?
} }
DbValueSessionStateV1::Never => SessionState::NeverExpires,
DbValueSessionStateV1::RevokedAt(dc) => SessionState::RevokedAt(Cid {
s_uuid: dc.server_id,
ts: dc.timestamp,
}),
};
rs_filter |= rs_uuid.as_u128();
Some((
refer,
Oauth2Session {
parent,
state,
issued_at,
rs_uuid,
},
))
} // End V3
} }
}) })
.collect(); .collect();
@ -1096,7 +1152,7 @@ impl ValueSetT for ValueSetOauth2Session {
DbValueSetV2::Oauth2Session( DbValueSetV2::Oauth2Session(
self.map self.map
.iter() .iter()
.map(|(u, m)| DbValueOauth2Session::V2 { .map(|(u, m)| DbValueOauth2Session::V3 {
refer: *u, refer: *u,
parent: m.parent, parent: m.parent,
state: match &m.state { state: match &m.state {
@ -1936,7 +1992,7 @@ mod tests {
Oauth2Session { Oauth2Session {
state: SessionState::NeverExpires, state: SessionState::NeverExpires,
issued_at: OffsetDateTime::now_utc(), issued_at: OffsetDateTime::now_utc(),
parent: Uuid::new_v4(), parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(), rs_uuid: Uuid::new_v4(),
}, },
); );
@ -1966,7 +2022,7 @@ mod tests {
Oauth2Session { Oauth2Session {
state: SessionState::NeverExpires, state: SessionState::NeverExpires,
issued_at: OffsetDateTime::now_utc(), issued_at: OffsetDateTime::now_utc(),
parent: Uuid::new_v4(), parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(), rs_uuid: Uuid::new_v4(),
}, },
); );
@ -1976,7 +2032,7 @@ mod tests {
Oauth2Session { Oauth2Session {
state: SessionState::RevokedAt(zero_cid.clone()), state: SessionState::RevokedAt(zero_cid.clone()),
issued_at: OffsetDateTime::now_utc(), issued_at: OffsetDateTime::now_utc(),
parent: Uuid::new_v4(), parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(), rs_uuid: Uuid::new_v4(),
}, },
); );
@ -2001,7 +2057,7 @@ mod tests {
Oauth2Session { Oauth2Session {
state: SessionState::NeverExpires, state: SessionState::NeverExpires,
issued_at: OffsetDateTime::now_utc(), issued_at: OffsetDateTime::now_utc(),
parent: Uuid::new_v4(), parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(), rs_uuid: Uuid::new_v4(),
}, },
); );
@ -2011,7 +2067,7 @@ mod tests {
Oauth2Session { Oauth2Session {
state: SessionState::RevokedAt(zero_cid.clone()), state: SessionState::RevokedAt(zero_cid.clone()),
issued_at: OffsetDateTime::now_utc(), issued_at: OffsetDateTime::now_utc(),
parent: Uuid::new_v4(), parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(), rs_uuid: Uuid::new_v4(),
}, },
); );
@ -2039,7 +2095,7 @@ mod tests {
Oauth2Session { Oauth2Session {
state: SessionState::NeverExpires, state: SessionState::NeverExpires,
issued_at: OffsetDateTime::now_utc(), issued_at: OffsetDateTime::now_utc(),
parent: Uuid::new_v4(), parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(), rs_uuid: Uuid::new_v4(),
}, },
); );
@ -2050,7 +2106,7 @@ mod tests {
Oauth2Session { Oauth2Session {
state: SessionState::RevokedAt(one_cid.clone()), state: SessionState::RevokedAt(one_cid.clone()),
issued_at: OffsetDateTime::now_utc(), issued_at: OffsetDateTime::now_utc(),
parent: Uuid::new_v4(), parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(), rs_uuid: Uuid::new_v4(),
}, },
), ),
@ -2059,7 +2115,7 @@ mod tests {
Oauth2Session { Oauth2Session {
state: SessionState::RevokedAt(zero_cid.clone()), state: SessionState::RevokedAt(zero_cid.clone()),
issued_at: OffsetDateTime::now_utc(), issued_at: OffsetDateTime::now_utc(),
parent: Uuid::new_v4(), parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(), rs_uuid: Uuid::new_v4(),
}, },
), ),
@ -2093,7 +2149,7 @@ mod tests {
Oauth2Session { Oauth2Session {
state: SessionState::NeverExpires, state: SessionState::NeverExpires,
issued_at: OffsetDateTime::now_utc(), issued_at: OffsetDateTime::now_utc(),
parent: Uuid::new_v4(), parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(), rs_uuid: Uuid::new_v4(),
}, },
); );
@ -2104,7 +2160,7 @@ mod tests {
Oauth2Session { Oauth2Session {
state: SessionState::RevokedAt(one_cid.clone()), state: SessionState::RevokedAt(one_cid.clone()),
issued_at: OffsetDateTime::now_utc(), issued_at: OffsetDateTime::now_utc(),
parent: Uuid::new_v4(), parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(), rs_uuid: Uuid::new_v4(),
}, },
), ),
@ -2113,7 +2169,7 @@ mod tests {
Oauth2Session { Oauth2Session {
state: SessionState::RevokedAt(zero_cid.clone()), state: SessionState::RevokedAt(zero_cid.clone()),
issued_at: OffsetDateTime::now_utc(), issued_at: OffsetDateTime::now_utc(),
parent: Uuid::new_v4(), parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(), rs_uuid: Uuid::new_v4(),
}, },
), ),
@ -2151,7 +2207,7 @@ mod tests {
Oauth2Session { Oauth2Session {
state: SessionState::RevokedAt(zero_cid), state: SessionState::RevokedAt(zero_cid),
issued_at: OffsetDateTime::now_utc(), issued_at: OffsetDateTime::now_utc(),
parent: Uuid::new_v4(), parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(), rs_uuid: Uuid::new_v4(),
}, },
), ),
@ -2160,7 +2216,7 @@ mod tests {
Oauth2Session { Oauth2Session {
state: SessionState::RevokedAt(one_cid), state: SessionState::RevokedAt(one_cid),
issued_at: OffsetDateTime::now_utc(), issued_at: OffsetDateTime::now_utc(),
parent: Uuid::new_v4(), parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(), rs_uuid: Uuid::new_v4(),
}, },
), ),
@ -2169,7 +2225,7 @@ mod tests {
Oauth2Session { Oauth2Session {
state: SessionState::RevokedAt(two_cid.clone()), state: SessionState::RevokedAt(two_cid.clone()),
issued_at: OffsetDateTime::now_utc(), issued_at: OffsetDateTime::now_utc(),
parent: Uuid::new_v4(), parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(), rs_uuid: Uuid::new_v4(),
}, },
), ),

View file

@ -1,5 +1,5 @@
#![deny(warnings)] #![deny(warnings)]
use std::collections::HashMap; use std::collections::{BTreeSet, HashMap};
use std::convert::TryFrom; use std::convert::TryFrom;
use std::str::FromStr; use std::str::FromStr;
@ -346,7 +346,7 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
let response = client let response = client
.post(rsclient.make_url("/oauth2/token/introspect")) .post(rsclient.make_url("/oauth2/token/introspect"))
.basic_auth("test_integration", Some(client_secret)) .basic_auth("test_integration", Some(client_secret.clone()))
.form(&intr_request) .form(&intr_request)
.send() .send()
.await .await
@ -415,6 +415,59 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
assert!(userinfo == oidc); assert!(userinfo == oidc);
// Step 6 - Show that our client can perform a client credentials grant
let form_req: AccessTokenRequest = GrantTypeReq::ClientCredentials {
scope: Some(BTreeSet::from([
"email".to_string(),
"read".to_string(),
"openid".to_string(),
])),
}
.into();
let response = client
.post(rsclient.make_url("/oauth2/token"))
.basic_auth("test_integration", Some(client_secret.clone()))
.form(&form_req)
.send()
.await
.expect("Failed to send client credentials request.");
assert!(response.status() == reqwest::StatusCode::OK);
let atr = response
.json::<AccessTokenResponse>()
.await
.expect("Unable to decode AccessTokenResponse");
// Step 7 - inspect the granted client credentials token.
let intr_request = AccessTokenIntrospectRequest {
token: atr.access_token.clone(),
token_type_hint: None,
};
let response = client
.post(rsclient.make_url("/oauth2/token/introspect"))
.basic_auth("test_integration", Some(client_secret))
.form(&intr_request)
.send()
.await
.expect("Failed to send token introspect request.");
assert!(response.status() == reqwest::StatusCode::OK);
let tir = response
.json::<AccessTokenIntrospectResponse>()
.await
.expect("Unable to decode AccessTokenIntrospectResponse");
assert!(tir.active);
assert!(tir.scope.is_some());
assert!(tir.client_id.as_deref() == Some("test_integration"));
assert!(tir.username.as_deref() == Some("test_integration@localhost"));
assert!(tir.token_type.as_deref() == Some("access_token"));
// auth back with admin so we can test deleting things // auth back with admin so we can test deleting things
let res = rsclient let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD) .auth_simple_password("admin", ADMIN_TEST_PASSWORD)