mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
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:
parent
492c3da36c
commit
d42268269a
|
@ -1,10 +1,10 @@
|
|||
use crate::{ClientError, KanidmClient};
|
||||
use kanidm_proto::constants::{
|
||||
ATTR_DISPLAYNAME, ATTR_ES256_PRIVATE_KEY_DER, ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE,
|
||||
ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT, ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE,
|
||||
ATTR_OAUTH2_PREFER_SHORT_USERNAME, ATTR_OAUTH2_RS_BASIC_SECRET, ATTR_OAUTH2_RS_NAME,
|
||||
ATTR_OAUTH2_RS_ORIGIN, ATTR_OAUTH2_RS_ORIGIN_LANDING, ATTR_OAUTH2_RS_TOKEN_KEY,
|
||||
ATTR_RS256_PRIVATE_KEY_DER,
|
||||
ATTR_DISPLAYNAME, ATTR_ES256_PRIVATE_KEY_DER, ATTR_NAME,
|
||||
ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE, ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT,
|
||||
ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE, ATTR_OAUTH2_PREFER_SHORT_USERNAME,
|
||||
ATTR_OAUTH2_RS_BASIC_SECRET, ATTR_OAUTH2_RS_ORIGIN, ATTR_OAUTH2_RS_ORIGIN_LANDING,
|
||||
ATTR_OAUTH2_RS_TOKEN_KEY, ATTR_RS256_PRIVATE_KEY_DER,
|
||||
};
|
||||
use kanidm_proto::internal::{ImageValue, Oauth2ClaimMapJoin};
|
||||
use kanidm_proto::v1::Entry;
|
||||
|
@ -27,7 +27,7 @@ impl KanidmClient {
|
|||
let mut new_oauth2_rs = Entry::default();
|
||||
new_oauth2_rs
|
||||
.attrs
|
||||
.insert(ATTR_OAUTH2_RS_NAME.to_string(), vec![name.to_string()]);
|
||||
.insert(ATTR_NAME.to_string(), vec![name.to_string()]);
|
||||
new_oauth2_rs
|
||||
.attrs
|
||||
.insert(ATTR_DISPLAYNAME.to_string(), vec![displayname.to_string()]);
|
||||
|
@ -47,7 +47,7 @@ impl KanidmClient {
|
|||
let mut new_oauth2_rs = Entry::default();
|
||||
new_oauth2_rs
|
||||
.attrs
|
||||
.insert(ATTR_OAUTH2_RS_NAME.to_string(), vec![name.to_string()]);
|
||||
.insert(ATTR_NAME.to_string(), vec![name.to_string()]);
|
||||
new_oauth2_rs
|
||||
.attrs
|
||||
.insert(ATTR_DISPLAYNAME.to_string(), vec![displayname.to_string()]);
|
||||
|
@ -91,7 +91,7 @@ impl KanidmClient {
|
|||
if let Some(newname) = name {
|
||||
update_oauth2_rs
|
||||
.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 {
|
||||
update_oauth2_rs.attrs.insert(
|
||||
|
|
|
@ -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_RADIUS_SECRET: &str = "radius_secret";
|
||||
pub const ATTR_RECYCLED: &str = "recycled";
|
||||
pub const ATTR_RECYCLEDDIRECTMEMBEROF: &str = "recycled_directmemberof";
|
||||
pub const ATTR_REPLICATED: &str = "replicated";
|
||||
pub const ATTR_RS256_PRIVATE_KEY_DER: &str = "rs256_private_key_der";
|
||||
pub const ATTR_SCOPE: &str = "scope";
|
||||
|
|
|
@ -85,6 +85,10 @@ pub enum GrantTypeReq {
|
|||
redirect_uri: Url,
|
||||
code_verifier: Option<String>,
|
||||
},
|
||||
ClientCredentials {
|
||||
#[serde_as(as = "Option<StringWithSeparator::<SpaceSeparator, String>>")]
|
||||
scope: Option<BTreeSet<String>>,
|
||||
},
|
||||
RefreshToken {
|
||||
refresh_token: String,
|
||||
#[serde_as(as = "Option<StringWithSeparator::<SpaceSeparator, String>>")]
|
||||
|
|
|
@ -10,8 +10,8 @@ class OAuth2Rs(BaseModel):
|
|||
classes: List[str]
|
||||
displayname: str
|
||||
es256_private_key_der: str
|
||||
name: str
|
||||
oauth2_rs_basic_secret: str
|
||||
oauth2_rs_name: str
|
||||
oauth2_rs_origin: str
|
||||
oauth2_rs_token_key: str
|
||||
oauth2_rs_sup_scope_map: List[str]
|
||||
|
@ -27,8 +27,8 @@ class RawOAuth2Rs(BaseModel):
|
|||
required_fields = (
|
||||
"displayname",
|
||||
"es256_private_key_der",
|
||||
"name",
|
||||
"oauth2_rs_basic_secret",
|
||||
"oauth2_rs_name",
|
||||
"oauth2_rs_origin",
|
||||
"oauth2_rs_token_key",
|
||||
)
|
||||
|
@ -42,8 +42,8 @@ class RawOAuth2Rs(BaseModel):
|
|||
classes=self.attrs["class"],
|
||||
displayname=self.attrs["displayname"][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_name=self.attrs["oauth2_rs_name"][0],
|
||||
oauth2_rs_origin=self.attrs["oauth2_rs_origin"][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", []),
|
||||
|
|
|
@ -75,7 +75,7 @@ impl IntoResponse for HTTPOauth2Error {
|
|||
pub(crate) fn oauth2_id(rs_name: &str) -> Filter<FilterInvalid> {
|
||||
filter_all!(f_and!([
|
||||
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))
|
||||
]))
|
||||
}
|
||||
|
||||
|
|
|
@ -57,6 +57,7 @@ pub(crate) async fn oauth2_basic_post(
|
|||
let classes = vec![
|
||||
EntryClass::OAuth2ResourceServer.to_string(),
|
||||
EntryClass::OAuth2ResourceServerBasic.to_string(),
|
||||
EntryClass::Account.to_string(),
|
||||
EntryClass::Object.to_string(),
|
||||
];
|
||||
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![
|
||||
EntryClass::OAuth2ResourceServer.to_string(),
|
||||
EntryClass::OAuth2ResourceServerPublic.to_string(),
|
||||
EntryClass::Account.to_string(),
|
||||
EntryClass::Object.to_string(),
|
||||
];
|
||||
json_rest_event_post(state, classes, obj, kopid, client_auth_info).await
|
||||
|
|
|
@ -617,7 +617,7 @@ async fn repl_acceptor(
|
|||
};
|
||||
|
||||
// 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
|
||||
// broadcast doesn't jam up because we aren't draining this task.
|
||||
drop(task_rx1);
|
||||
|
|
|
@ -544,6 +544,7 @@ pub enum DbValueApiToken {
|
|||
},
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum DbValueOauth2Session {
|
||||
V1 {
|
||||
|
@ -570,6 +571,18 @@ pub enum DbValueOauth2Session {
|
|||
#[serde(rename = "r")]
|
||||
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
|
||||
|
|
|
@ -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! {
|
||||
pub static ref IDM_ACP_DOMAIN_ADMIN_V1: BuiltinAcp = BuiltinAcp {
|
||||
classes: vec![
|
||||
|
|
|
@ -141,6 +141,7 @@ pub enum Attribute {
|
|||
PrivateCookieKey,
|
||||
PrivilegeExpiry,
|
||||
RadiusSecret,
|
||||
RecycledDirectMemberOf,
|
||||
Replicated,
|
||||
Rs256PrivateKeyDer,
|
||||
Scope,
|
||||
|
@ -329,6 +330,7 @@ impl TryFrom<String> for Attribute {
|
|||
ATTR_PRIVATE_COOKIE_KEY => Attribute::PrivateCookieKey,
|
||||
ATTR_PRIVILEGE_EXPIRY => Attribute::PrivilegeExpiry,
|
||||
ATTR_RADIUS_SECRET => Attribute::RadiusSecret,
|
||||
ATTR_RECYCLEDDIRECTMEMBEROF => Attribute::RecycledDirectMemberOf,
|
||||
ATTR_REPLICATED => Attribute::Replicated,
|
||||
ATTR_RS256_PRIVATE_KEY_DER => Attribute::Rs256PrivateKeyDer,
|
||||
ATTR_SCOPE => Attribute::Scope,
|
||||
|
@ -492,6 +494,7 @@ impl From<Attribute> for &'static str {
|
|||
Attribute::PrivateCookieKey => ATTR_PRIVATE_COOKIE_KEY,
|
||||
Attribute::PrivilegeExpiry => ATTR_PRIVILEGE_EXPIRY,
|
||||
Attribute::RadiusSecret => ATTR_RADIUS_SECRET,
|
||||
Attribute::RecycledDirectMemberOf => ATTR_RECYCLEDDIRECTMEMBEROF,
|
||||
Attribute::Replicated => ATTR_REPLICATED,
|
||||
Attribute::Rs256PrivateKeyDer => ATTR_RS256_PRIVATE_KEY_DER,
|
||||
Attribute::Scope => ATTR_SCOPE,
|
||||
|
@ -826,7 +829,6 @@ impl From<BuiltinAccount> for EntryInitNew {
|
|||
}
|
||||
|
||||
lazy_static! {
|
||||
|
||||
/// Builtin System Admin account.
|
||||
pub static ref BUILTIN_ACCOUNT_ADMIN: BuiltinAccount = BuiltinAccount {
|
||||
account_type: AccountType::ServiceAccount,
|
||||
|
@ -863,12 +865,18 @@ pub const UUID_TESTPERSON_2: Uuid = uuid!("538faac7-4d29-473b-a59d-23023ac19955"
|
|||
lazy_static! {
|
||||
pub static ref E_TESTPERSON_1: EntryInitNew = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(Attribute::DisplayName, Value::new_utf8s("Test Person 1")),
|
||||
(Attribute::Uuid, Value::Uuid(UUID_TESTPERSON_1))
|
||||
);
|
||||
pub static ref E_TESTPERSON_2: EntryInitNew = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson2")),
|
||||
(Attribute::DisplayName, Value::new_utf8s("Test Person 2")),
|
||||
(Attribute::Uuid, Value::Uuid(UUID_TESTPERSON_2))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -43,16 +43,18 @@ pub const SYSTEM_INDEX_VERSION: i64 = 30;
|
|||
*/
|
||||
pub type DomainVersion = u32;
|
||||
|
||||
pub const DOMAIN_LEVEL_0: DomainVersion = 0;
|
||||
pub const DOMAIN_LEVEL_1: DomainVersion = 1;
|
||||
pub const DOMAIN_LEVEL_2: DomainVersion = 2;
|
||||
pub const DOMAIN_LEVEL_3: DomainVersion = 3;
|
||||
pub const DOMAIN_LEVEL_4: DomainVersion = 4;
|
||||
pub const DOMAIN_LEVEL_5: DomainVersion = 5;
|
||||
// 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
|
||||
pub const DOMAIN_TGT_LEVEL: DomainVersion = DOMAIN_LEVEL_4;
|
||||
pub const DOMAIN_TGT_LEVEL: DomainVersion = DOMAIN_LEVEL_5;
|
||||
// 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
|
||||
#[cfg(test)]
|
||||
|
@ -65,15 +67,15 @@ pub const PURGE_FREQUENCY: u64 = 600;
|
|||
/// In test, we limit the changelog to 10 minutes.
|
||||
pub const CHANGELOG_MAX_AGE: u64 = 600;
|
||||
#[cfg(not(test))]
|
||||
/// A replica may be less than 1 day out of sync and catch up.
|
||||
pub const CHANGELOG_MAX_AGE: u64 = 86400;
|
||||
/// A replica may be up to 7 days out of sync before being denied updates.
|
||||
pub const CHANGELOG_MAX_AGE: u64 = 7 * 86400;
|
||||
|
||||
#[cfg(test)]
|
||||
/// In test, we limit the recyclebin to 5 minutes.
|
||||
pub const RECYCLEBIN_MAX_AGE: u64 = 300;
|
||||
#[cfg(not(test))]
|
||||
/// 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.
|
||||
pub const AUTH_SESSION_TIMEOUT: u64 = 300;
|
||||
|
|
|
@ -637,6 +637,32 @@ pub static ref SCHEMA_CLASS_PERSON: SchemaClass = SchemaClass {
|
|||
..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 {
|
||||
uuid: UUID_SCHEMA_CLASS_ORGPERSON,
|
||||
name: EntryClass::OrgPerson.into(),
|
||||
|
@ -725,7 +751,32 @@ pub static ref SCHEMA_CLASS_ACCOUNT: SchemaClass = SchemaClass {
|
|||
],
|
||||
systemsupplements: vec![
|
||||
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()
|
||||
};
|
||||
|
||||
|
@ -745,6 +796,27 @@ pub static ref SCHEMA_CLASS_SERVICE_ACCOUNT: SchemaClass = SchemaClass {
|
|||
..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 {
|
||||
uuid: UUID_SCHEMA_CLASS_SYNC_ACCOUNT,
|
||||
name: EntryClass::SyncAccount.into(),
|
||||
|
@ -877,6 +949,31 @@ pub static ref SCHEMA_CLASS_OAUTH2_RS_DL4: SchemaClass = SchemaClass {
|
|||
..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 {
|
||||
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS_BASIC,
|
||||
name: EntryClass::OAuth2ResourceServerBasic.into(),
|
||||
|
@ -888,6 +985,19 @@ pub static ref SCHEMA_CLASS_OAUTH2_RS_BASIC: SchemaClass = SchemaClass {
|
|||
..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 {
|
||||
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS_PUBLIC,
|
||||
name: EntryClass::OAuth2ResourceServerPublic.into(),
|
||||
|
|
|
@ -274,6 +274,8 @@ pub const UUID_SCHEMA_ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT: Uuid =
|
|||
uuid!("00000000-0000-0000-0000-ffff00000158");
|
||||
pub const UUID_SCHEMA_ATTR_OAUTH2_RS_CLAIM_MAP: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-ffff00000159");
|
||||
pub const UUID_SCHEMA_ATTR_RECYCLEDDIRECTMEMBEROF: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-ffff00000160");
|
||||
|
||||
// System and domain infos
|
||||
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
|
||||
|
|
|
@ -1202,6 +1202,7 @@ impl Entry<EntryInvalid, EntryCommitted> {
|
|||
self.remove_ava(Attribute::Class, &EntryClass::Recycled.into());
|
||||
self.remove_ava(Attribute::Class, &EntryClass::Conflict.into());
|
||||
self.purge_ava(Attribute::SourceUuid);
|
||||
self.purge_ava(Attribute::RecycledDirectMemberOf);
|
||||
|
||||
// Change state repl doesn't need this flag
|
||||
// self.valid.ecstate.revive(&self.valid.cid);
|
||||
|
@ -2312,6 +2313,25 @@ where
|
|||
&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
|
||||
/// can never be replicated after this.
|
||||
/// This is a TEST ONLY method and will never be exposed in production.
|
||||
|
|
|
@ -2008,8 +2008,8 @@ mod tests {
|
|||
|
||||
let e1 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(
|
||||
Attribute::Uuid,
|
||||
|
@ -2021,7 +2021,8 @@ mod tests {
|
|||
|
||||
let e2 = entry_init!(
|
||||
(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::Uuid,
|
||||
|
@ -2034,7 +2035,8 @@ mod tests {
|
|||
// We need to add these and then push through the state machine.
|
||||
let e_ts = entry_init!(
|
||||
(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::Uuid,
|
||||
|
@ -2082,7 +2084,7 @@ mod tests {
|
|||
let t_uuid = vs_refer![uuid!("a67c0c71-0b35-4218-a6b0-22d23d131d27")] as _;
|
||||
let r_uuid = server_txn.resolve_valueset(&t_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
|
||||
let t_uuid_non = vs_refer![uuid!("b83e98f0-3d2e-41d2-9796-d8d993289c86")] as _;
|
||||
|
|
|
@ -49,7 +49,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
|||
.cloned()?;
|
||||
|
||||
let name = entry
|
||||
.get_ava_single_iname(Attribute::OAuth2RsName)
|
||||
.get_ava_single_iname(Attribute::Name)
|
||||
.map(str::to_string)?;
|
||||
|
||||
Some(AppLink::Oauth2 {
|
||||
|
@ -84,6 +84,7 @@ mod tests {
|
|||
|
||||
let e_rs: Entry<EntryInit, EntryNew> = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(
|
||||
Attribute::Class,
|
||||
EntryClass::OAuth2ResourceServer.to_value()
|
||||
|
@ -92,10 +93,7 @@ mod tests {
|
|||
Attribute::Class,
|
||||
EntryClass::OAuth2ResourceServerBasic.to_value()
|
||||
),
|
||||
(
|
||||
Attribute::OAuth2RsName,
|
||||
Value::new_iname("test_resource_server")
|
||||
),
|
||||
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||
(
|
||||
Attribute::DisplayName,
|
||||
Value::new_utf8s("test_resource_server")
|
||||
|
|
|
@ -154,6 +154,14 @@ pub(crate) enum Oauth2TokenType {
|
|||
// We stash some details here for oidc.
|
||||
nonce: Option<String>,
|
||||
},
|
||||
ClientAccess {
|
||||
scopes: BTreeSet<String>,
|
||||
session_id: Uuid,
|
||||
uuid: Uuid,
|
||||
expiry: time::OffsetDateTime,
|
||||
iat: i64,
|
||||
nbf: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for Oauth2TokenType {
|
||||
|
@ -165,6 +173,9 @@ impl fmt::Display for Oauth2TokenType {
|
|||
Oauth2TokenType::Refresh { 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)>>,
|
||||
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.
|
||||
token_fernet: Fernet,
|
||||
jws_signer: Oauth2JwsSigner,
|
||||
|
@ -409,7 +422,7 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
|
|||
|
||||
// Now we know we can load the shared attrs.
|
||||
let name = ent
|
||||
.get_ava_single_iname(Attribute::OAuth2RsName)
|
||||
.get_ava_single_iname(Attribute::Name)
|
||||
.map(str::to_string)
|
||||
.ok_or(OperationError::InvalidValueState)?;
|
||||
|
||||
|
@ -449,6 +462,41 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
|
|||
.cloned()
|
||||
.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
|
||||
.get_ava_set(Attribute::OAuth2RsClaimMap)
|
||||
.and_then(|vs| vs.as_oauthclaim_map());
|
||||
|
@ -573,6 +621,8 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
|
|||
origin_https,
|
||||
scope_maps,
|
||||
sup_scope_maps,
|
||||
client_scopes,
|
||||
client_sup_scopes,
|
||||
claim_map,
|
||||
token_fernet,
|
||||
jws_signer,
|
||||
|
@ -641,7 +691,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
.token_fernet
|
||||
.decrypt(&revoke_req.token)
|
||||
.map_err(|_| {
|
||||
admin_error!("Failed to decrypt token introspection request");
|
||||
admin_error!("Failed to decrypt token revoke request");
|
||||
Oauth2Error::InvalidRequest
|
||||
})
|
||||
.and_then(|data| {
|
||||
|
@ -660,6 +710,12 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
uuid,
|
||||
..
|
||||
}
|
||||
| Oauth2TokenType::ClientAccess {
|
||||
session_id,
|
||||
expiry,
|
||||
uuid,
|
||||
..
|
||||
}
|
||||
| Oauth2TokenType::Refresh {
|
||||
session_id,
|
||||
expiry,
|
||||
|
@ -738,11 +794,13 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
};
|
||||
|
||||
// check the secret.
|
||||
match &o2rs.type_ {
|
||||
let client_authentication_valid = match &o2rs.type_ {
|
||||
OauthRSType::Basic { authz_secret, .. } => {
|
||||
match secret {
|
||||
Some(secret) => {
|
||||
if authz_secret != &secret {
|
||||
if authz_secret == &secret {
|
||||
true
|
||||
} else {
|
||||
security_info!("Invalid OAuth2 client_id secret");
|
||||
return Err(Oauth2Error::AuthenticationRequired);
|
||||
}
|
||||
|
@ -757,14 +815,10 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
}
|
||||
}
|
||||
// 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 ...
|
||||
|
||||
// TODO: add refresh token grant type.
|
||||
// If it's a refresh token grant, are the consent permissions the same?
|
||||
|
||||
match &token_req.grant_type {
|
||||
GrantTypeReq::AuthorizationCode {
|
||||
code,
|
||||
|
@ -777,6 +831,16 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
code_verifier.as_deref(),
|
||||
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 {
|
||||
refresh_token,
|
||||
scope,
|
||||
|
@ -1011,8 +1075,8 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
})?;
|
||||
|
||||
match token {
|
||||
Oauth2TokenType::Access { .. } => {
|
||||
admin_error!("attempt to refresh with access token");
|
||||
Oauth2TokenType::Access { .. } | Oauth2TokenType::ClientAccess { .. } => {
|
||||
admin_error!("attempt to refresh with an access token");
|
||||
Err(Oauth2Error::InvalidToken)
|
||||
}
|
||||
Oauth2TokenType::Refresh {
|
||||
|
@ -1035,7 +1099,13 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
// Check the session is still valid. This call checks the parent session
|
||||
// and the OAuth2 session.
|
||||
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"));
|
||||
|
||||
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(
|
||||
&mut self,
|
||||
o2rs: &Oauth2RS,
|
||||
|
@ -1271,7 +1446,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
let session = Value::Oauth2Session(
|
||||
session_id,
|
||||
Oauth2Session {
|
||||
parent: parent_session_id,
|
||||
parent: Some(parent_session_id),
|
||||
state: SessionState::ExpiresAt(refresh_expiry),
|
||||
issued_at: odt_ct,
|
||||
rs_uuid: o2rs.uuid,
|
||||
|
@ -1338,7 +1513,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
o2rs.token_fernet
|
||||
.decrypt(token)
|
||||
.map_err(|_| {
|
||||
admin_error!("Failed to decrypt token introspection request");
|
||||
admin_error!("Failed to decrypt token reflection request");
|
||||
OperationError::CryptographyError
|
||||
})
|
||||
.and_then(|data| {
|
||||
|
@ -1495,25 +1670,8 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
|||
return Err(Oauth2Error::InvalidRequest);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
// Validate all request scopes have valid syntax.
|
||||
validate_scopes(&req_scopes)?;
|
||||
|
||||
let uat_scopes: BTreeSet<String> = o2rs
|
||||
.scope_maps
|
||||
|
@ -1758,6 +1916,8 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
|||
|
||||
// We are authenticated! Yay! Now we can actually check things ...
|
||||
|
||||
let prefer_short_username = o2rs.prefer_short_username;
|
||||
|
||||
let token: Oauth2TokenType = o2rs
|
||||
.token_fernet
|
||||
.decrypt(&intr_req.token)
|
||||
|
@ -1793,13 +1953,19 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
|||
|
||||
// Is the user expired, or the OAuth2 session invalid?
|
||||
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"));
|
||||
|
||||
let Ok(Some(entry)) = valid else {
|
||||
security_info!(
|
||||
?uuid,
|
||||
"access token has no account not valid, returning inactive"
|
||||
"access token account is not valid, returning inactive"
|
||||
);
|
||||
return Ok(AccessTokenIntrospectResponse::inactive());
|
||||
};
|
||||
|
@ -1819,12 +1985,79 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
|||
|
||||
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());
|
||||
Ok(AccessTokenIntrospectResponse {
|
||||
active: true,
|
||||
scope,
|
||||
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,
|
||||
iat: Some(iat),
|
||||
exp: Some(exp),
|
||||
|
@ -1897,7 +2130,13 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
|||
|
||||
// Is the user expired, or the OAuth2 session invalid?
|
||||
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"));
|
||||
|
||||
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
|
||||
Oauth2TokenType::ClientAccess { .. } => {
|
||||
warn!("OpenID userinfo introspection of client access tokens is not currently supported.");
|
||||
Err(Oauth2Error::InvalidToken)
|
||||
}
|
||||
Oauth2TokenType::Refresh { .. } => Err(Oauth2Error::InvalidToken),
|
||||
}
|
||||
}
|
||||
|
@ -2252,6 +2495,30 @@ fn str_join(set: &BTreeSet<String>) -> String {
|
|||
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)]
|
||||
mod tests {
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
|
@ -2284,6 +2551,8 @@ mod tests {
|
|||
const UAT_EXPIRE: u64 = 5;
|
||||
const TOKEN_EXPIRE: u64 = 900;
|
||||
|
||||
const UUID_TESTGROUP: Uuid = uuid!("a3028223-bf20-47d5-8b65-967b5d2bb3eb");
|
||||
|
||||
macro_rules! create_code_verifier {
|
||||
($key:expr) => {{
|
||||
let code_verifier = $key.to_string();
|
||||
|
@ -2333,10 +2602,19 @@ mod tests {
|
|||
) -> (String, UserAuthToken, Identity, Uuid) {
|
||||
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::Account.to_value()),
|
||||
(
|
||||
Attribute::Class,
|
||||
EntryClass::OAuth2ResourceServer.to_value()
|
||||
|
@ -2345,11 +2623,8 @@ mod tests {
|
|||
Attribute::Class,
|
||||
EntryClass::OAuth2ResourceServerBasic.to_value()
|
||||
),
|
||||
(Attribute::Uuid, Value::Uuid(uuid)),
|
||||
(
|
||||
Attribute::OAuth2RsName,
|
||||
Value::new_iname("test_resource_server")
|
||||
),
|
||||
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
||||
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||
(
|
||||
Attribute::DisplayName,
|
||||
Value::new_utf8s("test_resource_server")
|
||||
|
@ -2362,7 +2637,7 @@ mod tests {
|
|||
(
|
||||
Attribute::OAuth2RsScopeMap,
|
||||
Value::new_oauthscopemap(
|
||||
UUID_SYSTEM_ADMINS,
|
||||
UUID_TESTGROUP,
|
||||
btreeset![OAUTH2_SCOPE_GROUPS.to_string()]
|
||||
)
|
||||
.expect("invalid oauthscope")
|
||||
|
@ -2396,12 +2671,12 @@ mod tests {
|
|||
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());
|
||||
|
||||
let entry = idms_prox_write
|
||||
.qs_write
|
||||
.internal_search_uuid(uuid)
|
||||
.internal_search_uuid(rs_uuid)
|
||||
.expect("Failed to retrieve OAuth2 resource entry ");
|
||||
let secret = entry
|
||||
.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*
|
||||
// the parent session to be valid and present!
|
||||
|
||||
let session_id = uuid::Uuid::new_v4();
|
||||
|
||||
let account = idms_prox_write
|
||||
.target_to_account(UUID_ADMIN)
|
||||
.target_to_account(UUID_TESTPERSON_1)
|
||||
.expect("account must exist");
|
||||
|
||||
let uat = account
|
||||
.to_userauthtoken(
|
||||
session_id,
|
||||
|
@ -2459,7 +2734,7 @@ mod tests {
|
|||
idms_prox_write
|
||||
.qs_write
|
||||
.internal_modify(
|
||||
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_ADMIN))),
|
||||
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
|
||||
&modlist,
|
||||
)
|
||||
.expect("Failed to modify user");
|
||||
|
@ -2470,7 +2745,7 @@ mod tests {
|
|||
|
||||
idms_prox_write.commit().expect("failed to commit");
|
||||
|
||||
(secret, uat, ident, uuid)
|
||||
(secret, uat, ident, rs_uuid)
|
||||
}
|
||||
|
||||
async fn setup_oauth2_resource_server_public(
|
||||
|
@ -2479,10 +2754,19 @@ mod tests {
|
|||
) -> (UserAuthToken, Identity, Uuid) {
|
||||
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::Account.to_value()),
|
||||
(
|
||||
Attribute::Class,
|
||||
EntryClass::OAuth2ResourceServer.to_value()
|
||||
|
@ -2491,11 +2775,8 @@ mod tests {
|
|||
Attribute::Class,
|
||||
EntryClass::OAuth2ResourceServerPublic.to_value()
|
||||
),
|
||||
(Attribute::Uuid, Value::Uuid(uuid)),
|
||||
(
|
||||
Attribute::OAuth2RsName,
|
||||
Value::new_iname("test_resource_server")
|
||||
),
|
||||
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
||||
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||
(
|
||||
Attribute::DisplayName,
|
||||
Value::new_utf8s("test_resource_server")
|
||||
|
@ -2507,7 +2788,7 @@ mod tests {
|
|||
// System admins
|
||||
(
|
||||
Attribute::OAuth2RsScopeMap,
|
||||
Value::new_oauthscopemap(UUID_SYSTEM_ADMINS, btreeset!["groups".to_string()])
|
||||
Value::new_oauthscopemap(UUID_TESTGROUP, btreeset!["groups".to_string()])
|
||||
.expect("invalid oauthscope")
|
||||
),
|
||||
(
|
||||
|
@ -2527,7 +2808,7 @@ mod tests {
|
|||
.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());
|
||||
|
||||
// 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 account = idms_prox_write
|
||||
.target_to_account(UUID_ADMIN)
|
||||
.target_to_account(UUID_TESTPERSON_1)
|
||||
.expect("account must exist");
|
||||
let uat = account
|
||||
.to_userauthtoken(
|
||||
|
@ -2581,7 +2862,7 @@ mod tests {
|
|||
idms_prox_write
|
||||
.qs_write
|
||||
.internal_modify(
|
||||
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_ADMIN))),
|
||||
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
|
||||
&modlist,
|
||||
)
|
||||
.expect("Failed to modify user");
|
||||
|
@ -2592,7 +2873,7 @@ mod tests {
|
|||
|
||||
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) {
|
||||
|
@ -3233,7 +3514,7 @@ mod tests {
|
|||
assert!(intr_response.active);
|
||||
assert!(intr_response.scope.as_deref() == Some("openid supplement"));
|
||||
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.iat == 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.
|
||||
let v_expire = Value::new_datetime_epoch(Duration::from_secs(TEST_CURRENT_TIME - 1));
|
||||
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(
|
||||
Attribute::AccountExpire.into(),
|
||||
v_expire,
|
||||
|
@ -3486,8 +3767,10 @@ mod tests {
|
|||
.expect("Failed to access internals of the refresh token");
|
||||
|
||||
let session_id = match reflected_token {
|
||||
Oauth2TokenType::Refresh { session_id, .. } => session_id,
|
||||
Oauth2TokenType::Access { session_id, .. } => session_id,
|
||||
Oauth2TokenType::Refresh { .. } | Oauth2TokenType::ClientAccess { .. } => {
|
||||
unreachable!()
|
||||
}
|
||||
};
|
||||
|
||||
assert!(idms_prox_write.commit().is_ok());
|
||||
|
@ -3498,7 +3781,7 @@ mod tests {
|
|||
// Check it is now there
|
||||
let entry = idms_prox_write
|
||||
.qs_write
|
||||
.internal_search_uuid(UUID_ADMIN)
|
||||
.internal_search_uuid(UUID_TESTPERSON_1)
|
||||
.expect("failed");
|
||||
let valid = entry
|
||||
.get_ava_as_oauth2session_map(Attribute::OAuth2Session)
|
||||
|
@ -3509,7 +3792,7 @@ mod tests {
|
|||
// Delete the resource server.
|
||||
|
||||
let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
|
||||
Attribute::OAuth2RsName,
|
||||
Attribute::Name,
|
||||
PartialValue::new_iname("test_resource_server")
|
||||
)));
|
||||
|
||||
|
@ -3520,7 +3803,7 @@ mod tests {
|
|||
// revoked.
|
||||
let entry = idms_prox_write
|
||||
.qs_write
|
||||
.internal_search_uuid(UUID_ADMIN)
|
||||
.internal_search_uuid(UUID_TESTPERSON_1)
|
||||
.expect("failed");
|
||||
let revoked = entry
|
||||
.get_ava_as_oauth2session_map(Attribute::OAuth2Session)
|
||||
|
@ -3953,7 +4236,7 @@ mod tests {
|
|||
== Url::parse("https://idm.example.com/oauth2/openid/test_resource_server")
|
||||
.unwrap()
|
||||
);
|
||||
assert!(oidc.sub == OidcSubject::U(UUID_ADMIN));
|
||||
assert!(oidc.sub == OidcSubject::U(UUID_TESTPERSON_1));
|
||||
assert!(oidc.aud == "test_resource_server");
|
||||
assert!(oidc.iat == iat);
|
||||
assert!(oidc.nbf == Some(iat));
|
||||
|
@ -3967,8 +4250,8 @@ mod tests {
|
|||
assert!(oidc.amr.is_none());
|
||||
assert!(oidc.azp == Some("test_resource_server".to_string()));
|
||||
assert!(oidc.jti.is_none());
|
||||
assert!(oidc.s_claims.name == Some("System Administrator".to_string()));
|
||||
assert!(oidc.s_claims.preferred_username == Some("admin@example.com".to_string()));
|
||||
assert!(oidc.s_claims.name == Some("Test Person 1".to_string()));
|
||||
assert!(oidc.s_claims.preferred_username == Some("testperson1@example.com".to_string()));
|
||||
assert!(
|
||||
oidc.s_claims.scopes == vec![OAUTH2_SCOPE_OPENID.to_string(), "supplement".to_string()]
|
||||
);
|
||||
|
@ -4119,7 +4402,7 @@ mod tests {
|
|||
.expect("Failed to verify oidc");
|
||||
|
||||
// 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?
|
||||
let userinfo = idms_prox_read
|
||||
.oauth2_openid_userinfo("test_resource_server", &access_token, ct)
|
||||
|
@ -4364,7 +4647,7 @@ mod tests {
|
|||
.verify_exp(iat)
|
||||
.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());
|
||||
}
|
||||
|
@ -4439,7 +4722,7 @@ mod tests {
|
|||
|
||||
let me_extend_scopes = ModifyEvent::new_internal_invalid(
|
||||
filter!(f_eq(
|
||||
Attribute::OAuth2RsName,
|
||||
Attribute::Name,
|
||||
PartialValue::new_iname("test_resource_server")
|
||||
)),
|
||||
ModifyList::new_list(vec![Modify::Present(
|
||||
|
@ -4505,7 +4788,7 @@ mod tests {
|
|||
|
||||
let me_extend_scopes = ModifyEvent::new_internal_invalid(
|
||||
filter!(f_eq(
|
||||
Attribute::OAuth2RsName,
|
||||
Attribute::Name,
|
||||
PartialValue::new_iname("test_resource_server")
|
||||
)),
|
||||
ModifyList::new_list(vec![Modify::Present(
|
||||
|
@ -4617,7 +4900,7 @@ mod tests {
|
|||
|
||||
// Now trigger the delete of the RS
|
||||
let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
|
||||
Attribute::OAuth2RsName,
|
||||
Attribute::Name,
|
||||
PartialValue::new_iname("test_resource_server")
|
||||
)));
|
||||
|
||||
|
@ -4935,7 +5218,7 @@ mod tests {
|
|||
|
||||
let refresh_exp = match reflected_token {
|
||||
Oauth2TokenType::Refresh { expiry, .. } => expiry.unix_timestamp(),
|
||||
Oauth2TokenType::Access { .. } => unreachable!(),
|
||||
Oauth2TokenType::Access { .. } | Oauth2TokenType::ClientAccess { .. } => unreachable!(),
|
||||
};
|
||||
|
||||
let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
|
||||
|
@ -5176,7 +5459,7 @@ mod tests {
|
|||
|
||||
let entry = idms_prox_write
|
||||
.qs_write
|
||||
.internal_search_uuid(UUID_ADMIN)
|
||||
.internal_search_uuid(UUID_TESTPERSON_1)
|
||||
.expect("failed");
|
||||
let valid = entry
|
||||
.get_ava_as_oauth2session_map(Attribute::OAuth2Session)
|
||||
|
@ -5295,7 +5578,7 @@ mod tests {
|
|||
Attribute::OAuth2RsClaimMap.into(),
|
||||
Value::OauthClaimValue(
|
||||
"custom_a".to_string(),
|
||||
UUID_SYSTEM_ADMINS,
|
||||
UUID_TESTGROUP,
|
||||
btreeset!["value_a".to_string()],
|
||||
),
|
||||
),
|
||||
|
@ -5320,7 +5603,7 @@ mod tests {
|
|||
Attribute::OAuth2RsClaimMap.into(),
|
||||
Value::OauthClaimValue(
|
||||
"custom_b".to_string(),
|
||||
UUID_SYSTEM_ADMINS,
|
||||
UUID_TESTGROUP,
|
||||
btreeset!["value_a".to_string()],
|
||||
),
|
||||
),
|
||||
|
@ -5434,7 +5717,7 @@ mod tests {
|
|||
== Url::parse("https://idm.example.com/oauth2/openid/test_resource_server")
|
||||
.unwrap()
|
||||
);
|
||||
assert!(oidc.sub == OidcSubject::U(UUID_ADMIN));
|
||||
assert!(oidc.sub == OidcSubject::U(UUID_TESTPERSON_1));
|
||||
assert!(oidc.aud == "test_resource_server");
|
||||
assert!(oidc.iat == iat);
|
||||
assert!(oidc.nbf == Some(iat));
|
||||
|
@ -5448,8 +5731,8 @@ mod tests {
|
|||
assert!(oidc.amr.is_none());
|
||||
assert!(oidc.azp == Some("test_resource_server".to_string()));
|
||||
assert!(oidc.jti.is_none());
|
||||
assert!(oidc.s_claims.name == Some("System Administrator".to_string()));
|
||||
assert!(oidc.s_claims.preferred_username == Some("admin@example.com".to_string()));
|
||||
assert!(oidc.s_claims.name == Some("Test Person 1".to_string()));
|
||||
assert!(oidc.s_claims.preferred_username == Some("testperson1@example.com".to_string()));
|
||||
assert!(
|
||||
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.scope.as_deref() == Some("openid supplement"));
|
||||
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.iat == 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());
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -628,7 +628,7 @@ pub trait IdmServerTransaction<'a> {
|
|||
&mut self,
|
||||
uuid: Uuid,
|
||||
session_id: Uuid,
|
||||
parent_session_id: Uuid,
|
||||
parent_session_id: Option<Uuid>,
|
||||
iat: i64,
|
||||
ct: Duration,
|
||||
) -> Result<Option<Arc<Entry<EntrySealed, EntryCommitted>>>, OperationError> {
|
||||
|
@ -661,9 +661,6 @@ pub trait IdmServerTransaction<'a> {
|
|||
let oauth2_session = entry
|
||||
.get_ava_as_oauth2session_map(Attribute::OAuth2Session)
|
||||
.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 {
|
||||
// We have the oauth2 session, lets check it.
|
||||
|
@ -674,24 +671,35 @@ pub trait IdmServerTransaction<'a> {
|
|||
return Ok(None);
|
||||
}
|
||||
|
||||
if let Some(uat_session) = uat_session {
|
||||
let parent_session_valid = !matches!(uat_session.state, SessionState::RevokedAt(_));
|
||||
if parent_session_valid {
|
||||
security_info!("A valid parent and oauth2 session value exists for this token");
|
||||
} else {
|
||||
// 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 {
|
||||
let parent_session_valid =
|
||||
!matches!(uat_session.state, SessionState::RevokedAt(_));
|
||||
if parent_session_valid {
|
||||
security_info!(
|
||||
"A valid parent and oauth2 session value exists for this token"
|
||||
);
|
||||
} else {
|
||||
security_info!(
|
||||
"The parent oauth2 session associated to this token is revoked."
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
} else if grace_valid {
|
||||
security_info!(
|
||||
"The parent oauth2 session associated to this token is revoked."
|
||||
"The token grace window is in effect. Assuming parent session valid."
|
||||
);
|
||||
} else {
|
||||
security_info!("The token grace window has passed and no entry parent sessions exist. Assuming invalid.");
|
||||
return Ok(None);
|
||||
}
|
||||
} else if grace_valid {
|
||||
security_info!(
|
||||
"The token grace window is in effect. Assuming parent session valid."
|
||||
);
|
||||
} else {
|
||||
security_info!("The token grace window has passed and no entry parent sessions exist. Assuming invalid.");
|
||||
return Ok(None);
|
||||
}
|
||||
// If we don't have a parent session id, we are good to proceed.
|
||||
} else if grace_valid {
|
||||
security_info!("The token grace window is in effect. Assuming valid.");
|
||||
} 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 cred = Credential::new_password_only(&p, pw)?;
|
||||
let cred_id = cred.uuid;
|
||||
let v_cred = Value::new_credential("primary", cred);
|
||||
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.
|
||||
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(
|
||||
Attribute::PrimaryCredential.into(),
|
||||
v_cred,
|
||||
|
@ -2353,7 +2369,7 @@ mod tests {
|
|||
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 admin_init = AuthEvent::named_init(name);
|
||||
|
||||
|
@ -2387,9 +2403,9 @@ mod tests {
|
|||
sessionid
|
||||
}
|
||||
|
||||
async fn check_admin_password(idms: &IdmServer, pw: &str) -> String {
|
||||
async fn check_testperson_password(idms: &IdmServer, pw: &str) -> String {
|
||||
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 anon_step = AuthEvent::cred_step_password(sid, pw);
|
||||
|
@ -2436,10 +2452,10 @@ mod tests {
|
|||
|
||||
#[idm_test]
|
||||
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
|
||||
.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
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
|
@ -2452,14 +2468,14 @@ mod tests {
|
|||
idms: &IdmServer,
|
||||
idms_delayed: &mut IdmServerDelayed,
|
||||
) {
|
||||
init_admin_w_password(idms, TEST_PASSWORD)
|
||||
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||
.await
|
||||
.expect("Failed to setup admin account");
|
||||
|
||||
let sid = init_admin_authsession_sid(
|
||||
let sid = init_authsession_sid(
|
||||
idms,
|
||||
Duration::from_secs(TEST_CURRENT_TIME),
|
||||
"admin@example.com",
|
||||
"testperson1@example.com",
|
||||
)
|
||||
.await;
|
||||
|
||||
|
@ -2513,11 +2529,11 @@ mod tests {
|
|||
_idms_delayed: &IdmServerDelayed,
|
||||
idms_audit: &mut IdmServerAudit,
|
||||
) {
|
||||
init_admin_w_password(idms, TEST_PASSWORD)
|
||||
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||
.await
|
||||
.expect("Failed to setup admin account");
|
||||
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 anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD_INC);
|
||||
|
||||
|
@ -2588,7 +2604,13 @@ mod tests {
|
|||
#[idm_test]
|
||||
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 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
|
||||
let r1 = idms_prox_write
|
||||
|
@ -2604,19 +2626,25 @@ mod tests {
|
|||
#[idm_test]
|
||||
async fn test_idm_radiusauthtoken(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
|
||||
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
|
||||
.regenerate_radius_secret(&rrse)
|
||||
.expect("Failed to reset radius credential 1");
|
||||
idms_prox_write.commit().expect("failed to commit");
|
||||
|
||||
let mut idms_prox_read = idms.proxy_read().await;
|
||||
let admin_entry = idms_prox_read
|
||||
let person_entry = idms_prox_read
|
||||
.qs_read
|
||||
.internal_search_uuid(UUID_ADMIN)
|
||||
.internal_search_uuid(UUID_TESTPERSON_1)
|
||||
.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
|
||||
.get_radiusauthtoken(&rate, duration_from_epoch_now())
|
||||
.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;
|
||||
// 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 =
|
||||
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(
|
||||
Attribute::PasswordImport.into(),
|
||||
Value::from("{SSHA512}JwrSUHkI7FTAfHRVR6KoFlSN0E3dmaQWARjZ+/UsShYlENOqDtFVU77HJLLrY2MuSp0jve52+pwtdVl2QUAHukQ0XUf5LDtM")
|
||||
|
@ -2796,18 +2830,18 @@ mod tests {
|
|||
idms_delayed.check_is_empty_or_panic();
|
||||
|
||||
let mut idms_prox_read = idms.proxy_read().await;
|
||||
let admin_entry = idms_prox_read
|
||||
let person_entry = idms_prox_read
|
||||
.qs_read
|
||||
.internal_search_uuid(UUID_ADMIN)
|
||||
.internal_search_uuid(UUID_TESTPERSON_1)
|
||||
.expect("Can't access admin entry.");
|
||||
let cred_before = admin_entry
|
||||
let cred_before = person_entry
|
||||
.get_ava_single_credential(Attribute::PrimaryCredential)
|
||||
.expect("No credential present")
|
||||
.clone();
|
||||
drop(idms_prox_read);
|
||||
|
||||
// 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
|
||||
// that on the pw upgrade that the credential uuid changes. This immediately
|
||||
|
@ -2827,11 +2861,11 @@ mod tests {
|
|||
assert!(Ok(true) == r);
|
||||
|
||||
let mut idms_prox_read = idms.proxy_read().await;
|
||||
let admin_entry = idms_prox_read
|
||||
let person_entry = idms_prox_read
|
||||
.qs_read
|
||||
.internal_search_uuid(UUID_ADMIN)
|
||||
.internal_search_uuid(UUID_TESTPERSON_1)
|
||||
.expect("Can't access admin entry.");
|
||||
let cred_after = admin_entry
|
||||
let cred_after = person_entry
|
||||
.get_ava_single_credential(Attribute::PrimaryCredential)
|
||||
.expect("No credential present")
|
||||
.clone();
|
||||
|
@ -2840,7 +2874,7 @@ mod tests {
|
|||
assert_eq!(cred_before.uuid, cred_after.uuid);
|
||||
|
||||
// Check the admin pw still matches
|
||||
check_admin_password(idms, "password").await;
|
||||
check_testperson_password(idms, "password").await;
|
||||
// Clear the next auth session record
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
|
||||
|
@ -2909,7 +2943,7 @@ mod tests {
|
|||
const TEST_EXPIRE_TIME: u64 = TEST_CURRENT_TIME + 120;
|
||||
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 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.
|
||||
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(Attribute::AccountExpire.into(), v_expire),
|
||||
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.
|
||||
|
||||
init_admin_w_password(idms, TEST_PASSWORD)
|
||||
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||
.await
|
||||
.expect("Failed to setup admin account");
|
||||
// Set the valid bounds high/low
|
||||
// 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_high = Duration::from_secs(TEST_AFTER_EXPIRY);
|
||||
|
@ -2996,10 +3030,10 @@ mod tests {
|
|||
_idms_delayed: &mut IdmServerDelayed,
|
||||
) {
|
||||
// Any account that is expired can't unix auth.
|
||||
init_admin_w_password(idms, TEST_PASSWORD)
|
||||
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||
.await
|
||||
.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_high = Duration::from_secs(TEST_AFTER_EXPIRY);
|
||||
|
@ -3007,7 +3041,7 @@ mod tests {
|
|||
// make the admin a valid posix account
|
||||
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await;
|
||||
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![
|
||||
Modify::Present(Attribute::Class.into(), EntryClass::PosixAccount.into()),
|
||||
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());
|
||||
|
||||
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.commit().is_ok());
|
||||
|
||||
// Now check auth when the time is too high or too low.
|
||||
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;
|
||||
// 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");
|
||||
// Also check the generated unix tokens are invalid.
|
||||
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
|
||||
.get_unixusertoken(&uute, time_low)
|
||||
.expect("Failed to generate unix user token");
|
||||
|
||||
assert!(tok_r.name == "admin");
|
||||
assert!(tok_r.name == "testperson1");
|
||||
assert!(!tok_r.valid);
|
||||
|
||||
let tok_r = idms_prox_read
|
||||
.get_unixusertoken(&uute, time_high)
|
||||
.expect("Failed to generate unix user token");
|
||||
|
||||
assert!(tok_r.name == "admin");
|
||||
assert!(tok_r.name == "testperson1");
|
||||
assert!(!tok_r.valid);
|
||||
}
|
||||
|
||||
|
@ -3065,16 +3099,16 @@ mod tests {
|
|||
) {
|
||||
// Any account not valid/expiry should not return
|
||||
// a radius packet.
|
||||
init_admin_w_password(idms, TEST_PASSWORD)
|
||||
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||
.await
|
||||
.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_high = Duration::from_secs(TEST_AFTER_EXPIRY);
|
||||
|
||||
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
|
||||
.regenerate_radius_secret(&rrse)
|
||||
.expect("Failed to reset radius credential 1");
|
||||
|
@ -3110,13 +3144,13 @@ mod tests {
|
|||
idms_delayed: &mut IdmServerDelayed,
|
||||
idms_audit: &mut IdmServerAudit,
|
||||
) {
|
||||
init_admin_w_password(idms, TEST_PASSWORD)
|
||||
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||
.await
|
||||
.expect("Failed to setup admin account");
|
||||
|
||||
// Auth invalid, no softlock present.
|
||||
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 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 invalid immediate, (ct < exp), autofail
|
||||
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
|
||||
.auth(
|
||||
|
@ -3208,9 +3242,12 @@ mod tests {
|
|||
// Tested in the softlock state machine.
|
||||
|
||||
// Auth valid once softlock pass, valid. Count remains.
|
||||
let sid =
|
||||
init_admin_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME + 2), "admin")
|
||||
.await;
|
||||
let sid = init_authsession_sid(
|
||||
idms,
|
||||
Duration::from_secs(TEST_CURRENT_TIME + 2),
|
||||
"testperson1",
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut idms_auth = idms.auth().await;
|
||||
let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD);
|
||||
|
@ -3269,17 +3306,17 @@ mod tests {
|
|||
_idms_delayed: &mut IdmServerDelayed,
|
||||
idms_audit: &mut IdmServerAudit,
|
||||
) {
|
||||
init_admin_w_password(idms, TEST_PASSWORD)
|
||||
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||
.await
|
||||
.expect("Failed to setup admin account");
|
||||
|
||||
// Start an *early* auth session.
|
||||
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
|
||||
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.
|
||||
let mut idms_auth = idms.auth().await;
|
||||
let anon_step = AuthEvent::cred_step_password(sid_later, TEST_PASSWORD_INC);
|
||||
|
@ -3364,13 +3401,13 @@ mod tests {
|
|||
idms: &IdmServer,
|
||||
_idms_delayed: &mut IdmServerDelayed,
|
||||
) {
|
||||
init_admin_w_password(idms, TEST_PASSWORD)
|
||||
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||
.await
|
||||
.expect("Failed to setup admin account");
|
||||
// make the admin a valid posix account
|
||||
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await;
|
||||
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![
|
||||
Modify::Present(Attribute::Class.into(), EntryClass::PosixAccount.into()),
|
||||
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());
|
||||
|
||||
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.commit().is_ok());
|
||||
|
||||
let mut idms_auth = idms.auth().await;
|
||||
let uuae_good = UnixUserAuthEvent::new_internal(UUID_ADMIN, TEST_PASSWORD);
|
||||
let uuae_bad = UnixUserAuthEvent::new_internal(UUID_ADMIN, TEST_PASSWORD_INC);
|
||||
let uuae_good = UnixUserAuthEvent::new_internal(UUID_TESTPERSON_1, TEST_PASSWORD);
|
||||
let uuae_bad = UnixUserAuthEvent::new_internal(UUID_TESTPERSON_1, TEST_PASSWORD_INC);
|
||||
|
||||
let a2 = idms_auth
|
||||
.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 expiry = ct + Duration::from_secs((DEFAULT_AUTH_SESSION_EXPIRY + 1).into());
|
||||
// Do an authenticate
|
||||
init_admin_w_password(idms, TEST_PASSWORD)
|
||||
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||
.await
|
||||
.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
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
|
@ -3460,7 +3497,7 @@ mod tests {
|
|||
let session_b = Uuid::new_v4();
|
||||
|
||||
// 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
|
||||
.expect("Failed to setup admin account");
|
||||
|
||||
|
@ -3468,14 +3505,14 @@ mod tests {
|
|||
let mut idms_prox_read = idms.proxy_read().await;
|
||||
let admin = idms_prox_read
|
||||
.qs_read
|
||||
.internal_search_uuid(UUID_ADMIN)
|
||||
.internal_search_uuid(UUID_TESTPERSON_1)
|
||||
.expect("failed");
|
||||
let sessions = admin.get_ava_as_session_map(Attribute::UserAuthTokenSession);
|
||||
assert!(sessions.is_none());
|
||||
drop(idms_prox_read);
|
||||
|
||||
let da = DelayedAction::AuthSessionRecord(AuthSessionRecord {
|
||||
target_uuid: UUID_ADMIN,
|
||||
target_uuid: UUID_TESTPERSON_1,
|
||||
session_id: session_a,
|
||||
cred_id,
|
||||
label: "Test Session A".to_string(),
|
||||
|
@ -3492,7 +3529,7 @@ mod tests {
|
|||
let mut idms_prox_read = idms.proxy_read().await;
|
||||
let admin = idms_prox_read
|
||||
.qs_read
|
||||
.internal_search_uuid(UUID_ADMIN)
|
||||
.internal_search_uuid(UUID_TESTPERSON_1)
|
||||
.expect("failed");
|
||||
let sessions = admin
|
||||
.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.
|
||||
|
||||
let da = DelayedAction::AuthSessionRecord(AuthSessionRecord {
|
||||
target_uuid: UUID_ADMIN,
|
||||
target_uuid: UUID_TESTPERSON_1,
|
||||
session_id: session_b,
|
||||
cred_id,
|
||||
label: "Test Session B".to_string(),
|
||||
|
@ -3522,7 +3559,7 @@ mod tests {
|
|||
let mut idms_prox_read = idms.proxy_read().await;
|
||||
let admin = idms_prox_read
|
||||
.qs_read
|
||||
.internal_search_uuid(UUID_ADMIN)
|
||||
.internal_search_uuid(UUID_TESTPERSON_1)
|
||||
.expect("failed");
|
||||
let sessions = admin
|
||||
.get_ava_as_session_map(Attribute::UserAuthTokenSession)
|
||||
|
@ -3556,10 +3593,10 @@ mod tests {
|
|||
assert!(post_grace < expiry);
|
||||
|
||||
// Do an authenticate
|
||||
init_admin_w_password(idms, TEST_PASSWORD)
|
||||
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||
.await
|
||||
.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.
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
|
@ -3922,10 +3959,10 @@ mod tests {
|
|||
) {
|
||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||
|
||||
init_admin_w_password(idms, TEST_PASSWORD)
|
||||
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||
.await
|
||||
.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
|
||||
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.commit().is_ok());
|
||||
// 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
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
|
|
|
@ -500,6 +500,7 @@ mod tests {
|
|||
fn test_pre_create_name_unique() {
|
||||
let e: Entry<EntryInit, EntryNew> = entry_init!(
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson")),
|
||||
(Attribute::Description, Value::new_utf8s("testperson")),
|
||||
(Attribute::DisplayName, Value::new_utf8s("testperson"))
|
||||
|
@ -524,6 +525,7 @@ mod tests {
|
|||
fn test_pre_create_name_unique_2() {
|
||||
let e: Entry<EntryInit, EntryNew> = entry_init!(
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson")),
|
||||
(Attribute::Description, Value::new_utf8s("testperson")),
|
||||
(Attribute::DisplayName, Value::new_utf8s("testperson"))
|
||||
|
|
|
@ -333,7 +333,7 @@ mod tests {
|
|||
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
||||
r#"{
|
||||
"attrs": {
|
||||
"class": ["person"],
|
||||
"class": ["person", "account"],
|
||||
"name": ["testperson"],
|
||||
"description": ["testperson"],
|
||||
"displayname": ["testperson"]
|
||||
|
@ -369,7 +369,7 @@ mod tests {
|
|||
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
||||
r#"{
|
||||
"attrs": {
|
||||
"class": ["person"],
|
||||
"class": ["person", "account"],
|
||||
"name": ["testperson"],
|
||||
"description": ["testperson"],
|
||||
"displayname": ["testperson"],
|
||||
|
@ -399,7 +399,7 @@ mod tests {
|
|||
let mut e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
||||
r#"{
|
||||
"attrs": {
|
||||
"class": ["person"],
|
||||
"class": ["person", "account"],
|
||||
"name": ["testperson"],
|
||||
"description": ["testperson"],
|
||||
"displayname": ["testperson"],
|
||||
|
@ -432,7 +432,7 @@ mod tests {
|
|||
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
||||
r#"{
|
||||
"attrs": {
|
||||
"class": ["person"],
|
||||
"class": ["person", "account"],
|
||||
"name": ["testperson"],
|
||||
"description": ["testperson"],
|
||||
"displayname": ["testperson"],
|
||||
|
@ -506,7 +506,7 @@ mod tests {
|
|||
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
||||
r#"{
|
||||
"attrs": {
|
||||
"class": ["person"],
|
||||
"class": ["account", "person"],
|
||||
"name": ["testperson"],
|
||||
"description": ["testperson"],
|
||||
"displayname": ["testperson"],
|
||||
|
|
|
@ -104,7 +104,7 @@ impl Domain {
|
|||
|
||||
// Setup the minimum functional level if one is not set already.
|
||||
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));
|
||||
warn!("plugin_domain: Applying domain version transform");
|
||||
} else {
|
||||
|
|
|
@ -244,6 +244,7 @@ impl DynGroup {
|
|||
pre_cand: &[Arc<Entry<EntrySealed, EntryCommitted>>],
|
||||
cand: &[Entry<EntrySealed, EntryCommitted>],
|
||||
_ident: &Identity,
|
||||
force_cand_updates: bool,
|
||||
) -> Result<Vec<Uuid>, OperationError> {
|
||||
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 it was a member.
|
||||
|
||||
trace!(?dyn_groups.insts);
|
||||
trace!(?force_cand_updates, ?dyn_groups.insts);
|
||||
|
||||
for (dg_uuid, dg_filter) in dyn_groups.insts.iter() {
|
||||
let dg_filter_valid = dg_filter
|
||||
|
@ -308,16 +308,26 @@ impl DynGroup {
|
|||
let pre_t = pre.entry_match_no_index(&dg_filter_valid);
|
||||
let post_t = post.entry_match_no_index(&dg_filter_valid);
|
||||
|
||||
if pre_t && !post_t {
|
||||
Some(Err(post.get_uuid()))
|
||||
} else if !pre_t && post_t {
|
||||
trace!(?post_t, ?force_cand_updates, ?pre_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()))
|
||||
} else if pre_t && !post_t {
|
||||
// The entry was deleted
|
||||
Some(Err(post.get_uuid()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
trace!(?matches);
|
||||
|
||||
if !matches.is_empty() {
|
||||
let filt = filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(*dg_uuid)));
|
||||
let mut work_set = qs.internal_search_writeable(&filt)?;
|
||||
|
@ -348,6 +358,8 @@ impl DynGroup {
|
|||
})?;
|
||||
}
|
||||
|
||||
trace!(?affected_uuids);
|
||||
|
||||
Ok(affected_uuids)
|
||||
}
|
||||
|
||||
|
|
|
@ -114,6 +114,7 @@ mod tests {
|
|||
let uuid = Uuid::new_v4();
|
||||
let e: Entry<EntryInit, EntryNew> = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(
|
||||
Attribute::Class,
|
||||
EntryClass::OAuth2ResourceServer.to_value()
|
||||
|
@ -127,10 +128,7 @@ mod tests {
|
|||
Attribute::DisplayName,
|
||||
Value::new_utf8s("test_resource_server")
|
||||
),
|
||||
(
|
||||
Attribute::OAuth2RsName,
|
||||
Value::new_iname("test_resource_server")
|
||||
),
|
||||
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||
(
|
||||
Attribute::OAuth2RsOrigin,
|
||||
Value::new_url_s("https://demo.example.com").unwrap()
|
||||
|
@ -168,6 +166,7 @@ mod tests {
|
|||
|
||||
let e: Entry<EntryInit, EntryNew> = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(
|
||||
Attribute::Class,
|
||||
EntryClass::OAuth2ResourceServer.to_value()
|
||||
|
@ -177,10 +176,7 @@ mod tests {
|
|||
EntryClass::OAuth2ResourceServerBasic.to_value()
|
||||
),
|
||||
(Attribute::Uuid, Value::Uuid(uuid)),
|
||||
(
|
||||
Attribute::OAuth2RsName,
|
||||
Value::new_iname("test_resource_server")
|
||||
),
|
||||
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||
(
|
||||
Attribute::DisplayName,
|
||||
Value::new_utf8s("test_resource_server")
|
||||
|
|
|
@ -235,12 +235,26 @@ impl Plugin for MemberOf {
|
|||
qs: &mut QueryServerWriteTransaction,
|
||||
pre_cand: &[Arc<EntrySealedCommitted>],
|
||||
cand: &[EntrySealedCommitted],
|
||||
_conflict_uuids: &BTreeSet<Uuid>,
|
||||
conflict_uuids: &BTreeSet<Uuid>,
|
||||
) -> 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
|
||||
// repl is internal and dyngroup has a safety check to prevent external triggers.
|
||||
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)]
|
||||
|
@ -250,7 +264,7 @@ impl Plugin for MemberOf {
|
|||
cand: &[Entry<EntrySealed, EntryCommitted>],
|
||||
me: &ModifyEvent,
|
||||
) -> 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)]
|
||||
|
@ -260,7 +274,29 @@ impl Plugin for MemberOf {
|
|||
cand: &[Entry<EntrySealed, EntryCommitted>],
|
||||
me: &BatchModifyEvent,
|
||||
) -> 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)]
|
||||
|
@ -368,11 +404,14 @@ impl Plugin for MemberOf {
|
|||
(None, None) => {
|
||||
// Ok
|
||||
}
|
||||
_ => {
|
||||
(entry_dmo, d_groups) => {
|
||||
admin_error!(
|
||||
"MemberOfInvalid directmemberof set and DMO search set differ in size: {}",
|
||||
e.get_uuid()
|
||||
);
|
||||
trace!(?e);
|
||||
trace!(?entry_dmo);
|
||||
trace!(?d_groups);
|
||||
r.push(Err(ConsistencyError::MemberOfInvalid(e.get_id())));
|
||||
}
|
||||
}
|
||||
|
@ -429,8 +468,15 @@ impl MemberOf {
|
|||
pre_cand: &[Arc<EntrySealedCommitted>],
|
||||
cand: &[EntrySealedCommitted],
|
||||
ident: &Identity,
|
||||
force_dyngroup_cand_update: bool,
|
||||
) -> 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.
|
||||
let group_affect = cand
|
||||
|
|
|
@ -338,7 +338,8 @@ impl Plugins {
|
|||
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
|
||||
de: &DeleteEvent,
|
||||
) -> 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)]
|
||||
|
|
|
@ -397,6 +397,7 @@ mod tests {
|
|||
Attribute::PrivateCookieKey.to_value()
|
||||
),
|
||||
(Attribute::AcpCreateClass, EntryClass::Object.to_value()),
|
||||
(Attribute::AcpCreateClass, EntryClass::Account.to_value()),
|
||||
(Attribute::AcpCreateClass, EntryClass::Person.to_value()),
|
||||
(Attribute::AcpCreateClass, EntryClass::System.to_value()),
|
||||
(Attribute::AcpCreateClass, EntryClass::DomainInfo.to_value()),
|
||||
|
@ -438,7 +439,7 @@ mod tests {
|
|||
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
||||
r#"{
|
||||
"attrs": {
|
||||
"class": ["person", "system"],
|
||||
"class": ["account", "person", "system"],
|
||||
"name": ["testperson"],
|
||||
"description": ["testperson"],
|
||||
"displayname": ["testperson"]
|
||||
|
@ -464,7 +465,7 @@ mod tests {
|
|||
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
||||
r#"{
|
||||
"attrs": {
|
||||
"class": ["person", "system"],
|
||||
"class": ["account", "person", "system"],
|
||||
"name": ["testperson"],
|
||||
"description": ["testperson"],
|
||||
"displayname": ["testperson"]
|
||||
|
@ -526,7 +527,7 @@ mod tests {
|
|||
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
||||
r#"{
|
||||
"attrs": {
|
||||
"class": ["person", "system"],
|
||||
"class": ["account", "person", "system"],
|
||||
"name": ["testperson"],
|
||||
"description": ["testperson"],
|
||||
"displayname": ["testperson"]
|
||||
|
|
|
@ -990,15 +990,13 @@ mod tests {
|
|||
// scope map is also appropriately affected.
|
||||
let ea: Entry<EntryInit, EntryNew> = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(
|
||||
Attribute::Class,
|
||||
EntryClass::OAuth2ResourceServer.to_value()
|
||||
),
|
||||
// (Attribute::Class, EntryClass::OAuth2ResourceServerBasic.into()),
|
||||
(
|
||||
Attribute::OAuth2RsName,
|
||||
Value::new_iname("test_resource_server")
|
||||
),
|
||||
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||
(
|
||||
Attribute::DisplayName,
|
||||
Value::new_utf8s("test_resource_server")
|
||||
|
@ -1037,7 +1035,7 @@ mod tests {
|
|||
|qs: &mut QueryServerWriteTransaction| {
|
||||
let cands = qs
|
||||
.internal_search(filter!(f_eq(
|
||||
Attribute::OAuth2RsName,
|
||||
Attribute::Name,
|
||||
PartialValue::new_iname("test_resource_server")
|
||||
)))
|
||||
.expect("Internal search failure");
|
||||
|
@ -1080,15 +1078,13 @@ mod tests {
|
|||
|
||||
let e2 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(
|
||||
Attribute::Class,
|
||||
EntryClass::OAuth2ResourceServer.to_value()
|
||||
),
|
||||
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
||||
(
|
||||
Attribute::OAuth2RsName,
|
||||
Value::new_iname("test_resource_server")
|
||||
),
|
||||
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||
(
|
||||
Attribute::DisplayName,
|
||||
Value::new_utf8s("test_resource_server")
|
||||
|
@ -1129,7 +1125,7 @@ mod tests {
|
|||
Value::Oauth2Session(
|
||||
session_id,
|
||||
Oauth2Session {
|
||||
parent: parent_id,
|
||||
parent: Some(parent_id),
|
||||
// Note we set the exp to None so we are not removing based on exp
|
||||
state: SessionState::NeverExpires,
|
||||
issued_at,
|
||||
|
@ -1309,6 +1305,7 @@ mod tests {
|
|||
fn test_delete_remove_reference_oauth2_claim_map() {
|
||||
let ea: Entry<EntryInit, EntryNew> = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(
|
||||
Attribute::Class,
|
||||
EntryClass::OAuth2ResourceServer.to_value()
|
||||
|
@ -1317,10 +1314,7 @@ mod tests {
|
|||
Attribute::Class,
|
||||
EntryClass::OAuth2ResourceServerPublic.to_value()
|
||||
),
|
||||
(
|
||||
Attribute::OAuth2RsName,
|
||||
Value::new_iname("test_resource_server")
|
||||
),
|
||||
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||
(
|
||||
Attribute::DisplayName,
|
||||
Value::new_utf8s("test_resource_server")
|
||||
|
@ -1366,7 +1360,7 @@ mod tests {
|
|||
|qs: &mut QueryServerWriteTransaction| {
|
||||
let cands = qs
|
||||
.internal_search(filter!(f_eq(
|
||||
Attribute::OAuth2RsName,
|
||||
Attribute::Name,
|
||||
PartialValue::new_iname("test_resource_server")
|
||||
)))
|
||||
.expect("Internal search failure");
|
||||
|
|
|
@ -131,12 +131,19 @@ impl SessionConsistency {
|
|||
_ => {
|
||||
// Okay, now check the issued / grace time for parent enforcement.
|
||||
if sessions.map(|session_map| {
|
||||
if let Some(parent_session) = session_map.get(&session.parent) {
|
||||
// Only match non-revoked sessions
|
||||
!matches!(parent_session.state, SessionState::RevokedAt(_))
|
||||
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
|
||||
!matches!(parent_session.state, SessionState::RevokedAt(_))
|
||||
} else {
|
||||
// not found
|
||||
false
|
||||
}
|
||||
} else {
|
||||
// not found
|
||||
false
|
||||
// The session specifically has no parent session and so is
|
||||
// not bounded by it's presence.
|
||||
true
|
||||
}
|
||||
}).unwrap_or(false) {
|
||||
// The parent exists and is still valid, go ahead
|
||||
|
@ -145,7 +152,7 @@ impl SessionConsistency {
|
|||
} else {
|
||||
// Can't find the parent. Are we within grace window
|
||||
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))
|
||||
} else {
|
||||
// Grace window is still in effect
|
||||
|
@ -330,6 +337,7 @@ mod tests {
|
|||
|
||||
let e2 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(
|
||||
Attribute::Class,
|
||||
EntryClass::OAuth2ResourceServer.to_value()
|
||||
|
@ -339,10 +347,7 @@ mod tests {
|
|||
EntryClass::OAuth2ResourceServerBasic.to_value()
|
||||
),
|
||||
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
||||
(
|
||||
Attribute::OAuth2RsName,
|
||||
Value::new_iname("test_resource_server")
|
||||
),
|
||||
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||
(
|
||||
Attribute::DisplayName,
|
||||
Value::new_utf8s("test_resource_server")
|
||||
|
@ -381,7 +386,7 @@ mod tests {
|
|||
Value::Oauth2Session(
|
||||
session_id,
|
||||
Oauth2Session {
|
||||
parent: parent_id,
|
||||
parent: Some(parent_id),
|
||||
// Set to the exp window.
|
||||
state,
|
||||
issued_at,
|
||||
|
@ -505,6 +510,7 @@ mod tests {
|
|||
|
||||
let e2 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(
|
||||
Attribute::Class,
|
||||
EntryClass::OAuth2ResourceServer.to_value()
|
||||
|
@ -514,10 +520,7 @@ mod tests {
|
|||
EntryClass::OAuth2ResourceServerBasic.to_value()
|
||||
),
|
||||
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
||||
(
|
||||
Attribute::OAuth2RsName,
|
||||
Value::new_iname("test_resource_server")
|
||||
),
|
||||
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||
(
|
||||
Attribute::DisplayName,
|
||||
Value::new_utf8s("test_resource_server")
|
||||
|
@ -555,7 +558,7 @@ mod tests {
|
|||
Value::Oauth2Session(
|
||||
session_id,
|
||||
Oauth2Session {
|
||||
parent: parent_id,
|
||||
parent: Some(parent_id),
|
||||
// Note we set the exp to None so we are not removing based on exp
|
||||
state: SessionState::NeverExpires,
|
||||
issued_at,
|
||||
|
@ -673,6 +676,7 @@ mod tests {
|
|||
|
||||
let e2 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(
|
||||
Attribute::Class,
|
||||
EntryClass::OAuth2ResourceServer.to_value()
|
||||
|
@ -682,10 +686,7 @@ mod tests {
|
|||
EntryClass::OAuth2ResourceServerBasic.to_value()
|
||||
),
|
||||
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
||||
(
|
||||
Attribute::OAuth2RsName,
|
||||
Value::new_iname("test_resource_server")
|
||||
),
|
||||
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||
(
|
||||
Attribute::DisplayName,
|
||||
Value::new_utf8s("test_resource_server")
|
||||
|
@ -716,7 +717,7 @@ mod tests {
|
|||
let session = Value::Oauth2Session(
|
||||
session_id,
|
||||
Oauth2Session {
|
||||
parent,
|
||||
parent: Some(parent),
|
||||
// Note we set the exp to None so we are asserting the removal is due to the lack
|
||||
// of the parent session.
|
||||
state: SessionState::NeverExpires,
|
||||
|
|
|
@ -136,6 +136,7 @@ mod tests {
|
|||
assert!(server_txn
|
||||
.internal_create(vec![entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("tobias")),
|
||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||
|
@ -154,6 +155,7 @@ mod tests {
|
|||
assert!(server_txn
|
||||
.internal_create(vec![entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("newname")),
|
||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||
|
@ -181,6 +183,7 @@ mod tests {
|
|||
assert!(server_txn
|
||||
.internal_create(vec![entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("newname")),
|
||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||
|
|
|
@ -184,10 +184,18 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
//
|
||||
let (cand, pre_cand): (Vec<_>, Vec<_>) = all_updates_valid
|
||||
.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, _)| {
|
||||
// Exclude anything that is conflicted as a result of the conflict plugins.
|
||||
!conflict_uuids.contains(&cand.get_uuid())
|
||||
})
|
||||
*/
|
||||
.unzip();
|
||||
|
||||
// We don't need to process conflict_creates here, since they are all conflicting
|
||||
|
|
|
@ -215,9 +215,9 @@ impl EntryChangeState {
|
|||
}
|
||||
|
||||
#[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 {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ use crate::schema::{SchemaReadTransaction, SchemaTransaction};
|
|||
use crate::valueset;
|
||||
use base64urlsafedata::Base64UrlSafeData;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::skip_serializing_none;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use webauthn_rs::prelude::{
|
||||
|
@ -261,10 +262,11 @@ pub struct ReplOauthClaimMapV1 {
|
|||
pub values: BTreeMap<Uuid, BTreeSet<String>>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
pub struct ReplOauth2SessionV1 {
|
||||
pub refer: Uuid,
|
||||
pub parent: Uuid,
|
||||
pub parent: Option<Uuid>,
|
||||
pub state: ReplSessionStateV1,
|
||||
// pub expiry: Option<String>,
|
||||
pub issued_at: String,
|
||||
|
|
|
@ -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() {
|
||||
debug!("No Changes Available");
|
||||
return Ok(ReplIncrementalContext::NoChangesAvailable);
|
||||
}
|
||||
|
||||
|
|
|
@ -250,6 +250,7 @@ async fn test_repl_increment_basic_entry_add(server_a: &QueryServer, server_b: &
|
|||
assert!(server_b_txn
|
||||
.internal_create(vec![entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(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
|
||||
.internal_create(vec![entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(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
|
||||
.internal_create(vec![entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||
|
@ -481,6 +484,7 @@ async fn test_repl_increment_consumer_lagging_tombstone(
|
|||
assert!(server_b_txn
|
||||
.internal_create(vec![entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||
|
@ -584,6 +588,7 @@ async fn test_repl_increment_basic_bidirectional_write(
|
|||
assert!(server_b_txn
|
||||
.internal_create(vec![entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(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
|
||||
.internal_create(vec![entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||
|
@ -731,6 +737,7 @@ async fn test_repl_increment_simultaneous_bidirectional_write(
|
|||
assert!(server_b_txn
|
||||
.internal_create(vec![entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||
|
@ -842,6 +849,7 @@ async fn test_repl_increment_basic_bidirectional_lifecycle(
|
|||
assert!(server_b_txn
|
||||
.internal_create(vec![entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||
|
@ -983,6 +991,7 @@ async fn test_repl_increment_basic_bidirectional_recycle(
|
|||
assert!(server_b_txn
|
||||
.internal_create(vec![entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||
|
@ -1109,6 +1118,7 @@ async fn test_repl_increment_basic_bidirectional_tombstone(
|
|||
assert!(server_b_txn
|
||||
.internal_create(vec![entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(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 e_init = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||
|
@ -1244,6 +1255,19 @@ async fn test_repl_increment_creation_uuid_conflict(
|
|||
.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 0");
|
||||
trace!(?e1);
|
||||
trace!(?e2);
|
||||
trace!(?e1_acc);
|
||||
trace!(?e2_acc);
|
||||
|
||||
trace!("{:?}", e1.get_last_changed());
|
||||
trace!("{:?}", e2.get_last_changed());
|
||||
// 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)
|
||||
.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());
|
||||
|
||||
let cnf_a = server_a_txn
|
||||
|
@ -1292,6 +1329,10 @@ async fn test_repl_increment_creation_uuid_conflict(
|
|||
.expect("Unable to conflict entries.");
|
||||
assert!(cnf_b.is_empty());
|
||||
|
||||
trace!("TESTMARKER 2");
|
||||
trace!(?cnf_a);
|
||||
trace!(?cnf_b);
|
||||
|
||||
server_a_txn.commit().expect("Failed to commit");
|
||||
drop(server_b_txn);
|
||||
|
||||
|
@ -1319,8 +1360,32 @@ async fn test_repl_increment_creation_uuid_conflict(
|
|||
.pop()
|
||||
.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());
|
||||
|
||||
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");
|
||||
drop(server_a_txn);
|
||||
}
|
||||
|
@ -1344,6 +1409,7 @@ async fn test_repl_increment_create_tombstone_uuid_conflict(
|
|||
let t_uuid = Uuid::new_v4();
|
||||
let e_init = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(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 e_init = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(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
|
||||
.internal_create(vec![entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(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 modlist = ModifyList::new_list(vec![
|
||||
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::Purged(Attribute::IdVerificationEcKey.into()),
|
||||
Modify::Purged(Attribute::NameHistory.into()),
|
||||
Modify::Purged(Attribute::DisplayName.into()),
|
||||
]);
|
||||
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
|
||||
.internal_create(vec![entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||
|
@ -1775,6 +1846,7 @@ async fn test_repl_increment_consumer_ruv_trim_past_valid(
|
|||
assert!(server_b_txn
|
||||
.internal_create(vec![entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||
|
@ -1905,6 +1977,7 @@ async fn test_repl_increment_consumer_ruv_trim_idle_servers(
|
|||
assert!(server_b_txn
|
||||
.internal_create(vec![entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||
|
|
|
@ -1433,6 +1433,25 @@ impl<'a> SchemaWriteTransaction<'a> {
|
|||
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(
|
||||
Attribute::Member.into(),
|
||||
SchemaAttribute {
|
||||
|
@ -1974,6 +1993,7 @@ impl<'a> SchemaWriteTransaction<'a> {
|
|||
name: EntryClass::Recycled.into(),
|
||||
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."),
|
||||
systemmay: vec![Attribute::RecycledDirectMemberOf.into()],
|
||||
.. Default::default()
|
||||
},
|
||||
);
|
||||
|
|
|
@ -2719,10 +2719,7 @@ mod tests {
|
|||
EntryClass::OAuth2ResourceServerBasic.to_value()
|
||||
),
|
||||
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
||||
(
|
||||
Attribute::OAuth2RsName,
|
||||
Value::new_iname("test_resource_server")
|
||||
),
|
||||
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||
(
|
||||
Attribute::DisplayName,
|
||||
Value::new_utf8s("test_resource_server")
|
||||
|
@ -2764,10 +2761,7 @@ mod tests {
|
|||
EntryClass::OAuth2ResourceServerBasic.to_value()
|
||||
),
|
||||
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
||||
(
|
||||
Attribute::OAuth2RsName,
|
||||
Value::new_iname("test_resource_server")
|
||||
),
|
||||
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||
(
|
||||
Attribute::DisplayName,
|
||||
Value::new_utf8s("test_resource_server")
|
||||
|
@ -2790,10 +2784,7 @@ mod tests {
|
|||
EntryClass::OAuth2ResourceServerBasic.to_value()
|
||||
),
|
||||
(Attribute::Uuid, Value::Uuid(Uuid::new_v4())),
|
||||
(
|
||||
Attribute::OAuth2RsName,
|
||||
Value::new_iname("second_resource_server")
|
||||
),
|
||||
(Attribute::Name, Value::new_iname("second_resource_server")),
|
||||
(
|
||||
Attribute::DisplayName,
|
||||
Value::new_utf8s("second_resource_server")
|
||||
|
@ -2832,14 +2823,14 @@ mod tests {
|
|||
|
||||
let se_a = SearchEvent::new_impersonate_entry(
|
||||
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_reduced = vec![ev1_reduced];
|
||||
|
||||
let se_b = SearchEvent::new_impersonate_entry(
|
||||
E_TEST_ACCOUNT_2.clone(),
|
||||
filter_all!(f_pres(Attribute::OAuth2RsName)),
|
||||
filter_all!(f_pres(Attribute::Name)),
|
||||
);
|
||||
let ex_b = vec![];
|
||||
|
||||
|
|
|
@ -191,7 +191,7 @@ fn search_oauth2_filter_entry<'a>(
|
|||
Attribute::Class.as_ref(),
|
||||
Attribute::DisplayName.as_ref(),
|
||||
Attribute::Uuid.as_ref(),
|
||||
Attribute::OAuth2RsName.as_ref(),
|
||||
Attribute::Name.as_ref(),
|
||||
Attribute::OAuth2RsOrigin.as_ref(),
|
||||
Attribute::OAuth2RsOriginLanding.as_ref(),
|
||||
Attribute::Image.as_ref()
|
||||
|
|
|
@ -204,6 +204,7 @@ mod tests {
|
|||
|
||||
let e1 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(
|
||||
|
@ -216,6 +217,7 @@ mod tests {
|
|||
|
||||
let e2 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson2")),
|
||||
(
|
||||
|
@ -228,6 +230,7 @@ mod tests {
|
|||
|
||||
let e3 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson3")),
|
||||
(
|
||||
|
|
|
@ -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)]
|
||||
pub fn initialise_schema_core(&mut self) -> Result<(), OperationError> {
|
||||
admin_debug!("initialise_schema_core -> start ...");
|
||||
|
|
|
@ -1159,9 +1159,9 @@ impl QueryServer {
|
|||
|
||||
let d_info = Arc::new(CowCell::new(DomainInfo {
|
||||
d_uuid,
|
||||
// Start with our minimum supported level.
|
||||
// Start with our level as zero.
|
||||
// This will be reloaded from the DB shortly :)
|
||||
d_vers: DOMAIN_MIN_LEVEL,
|
||||
d_vers: DOMAIN_LEVEL_0,
|
||||
d_name: domain_name.clone(),
|
||||
// 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.
|
||||
|
@ -1630,6 +1630,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
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(())
|
||||
}
|
||||
|
||||
|
@ -1853,6 +1857,7 @@ mod tests {
|
|||
assert!(server_txn
|
||||
.internal_create(vec![entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||
|
@ -1983,6 +1988,7 @@ mod tests {
|
|||
let mut server_txn = server.write(duration_from_epoch_now()).await;
|
||||
let e1 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(
|
||||
|
|
|
@ -209,9 +209,21 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
if !self.changed_oauth2 {
|
||||
self.changed_oauth2 = norm_cand
|
||||
.iter()
|
||||
.chain(pre_candidates.iter().map(|e| e.as_ref()))
|
||||
.any(|e| {
|
||||
e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into())
|
||||
.zip(pre_candidates.iter().map(|e| e.as_ref()))
|
||||
.any(|(post, pre)| {
|
||||
// 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 {
|
||||
|
@ -511,6 +523,7 @@ mod tests {
|
|||
|
||||
let e1 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(
|
||||
|
@ -523,6 +536,7 @@ mod tests {
|
|||
|
||||
let e2 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson2")),
|
||||
(
|
||||
|
@ -671,6 +685,7 @@ mod tests {
|
|||
|
||||
let e1 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(
|
||||
|
|
|
@ -148,7 +148,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
// Get this entries 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 {
|
||||
dm_mods
|
||||
.entry(g_uuid)
|
||||
|
@ -291,6 +291,7 @@ mod tests {
|
|||
// Create some recycled objects
|
||||
let e1 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(
|
||||
|
@ -303,6 +304,7 @@ mod tests {
|
|||
|
||||
let e2 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson2")),
|
||||
(
|
||||
|
@ -397,6 +399,7 @@ mod tests {
|
|||
|
||||
let e1 = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(
|
||||
|
@ -532,6 +535,7 @@ mod tests {
|
|||
// First, create an entry, then push it through the lifecycle.
|
||||
let e_ts = entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname("testperson1")),
|
||||
(
|
||||
|
@ -610,6 +614,7 @@ mod tests {
|
|||
fn create_user(name: &str, uuid: &str) -> Entry<EntryInit, EntryNew> {
|
||||
entry_init!(
|
||||
(Attribute::Class, EntryClass::Object.to_value()),
|
||||
(Attribute::Class, EntryClass::Account.to_value()),
|
||||
(Attribute::Class, EntryClass::Person.to_value()),
|
||||
(Attribute::Name, Value::new_iname(name)),
|
||||
(
|
||||
|
@ -639,7 +644,7 @@ mod tests {
|
|||
}
|
||||
|
||||
fn check_entry_has_mo(qs: &mut QueryServerWriteTransaction, name: &str, mo: &str) -> bool {
|
||||
let e = qs
|
||||
let entry = qs
|
||||
.internal_search(filter!(f_eq(
|
||||
Attribute::Name,
|
||||
PartialValue::new_iname(name)
|
||||
|
@ -648,7 +653,9 @@ mod tests {
|
|||
.pop()
|
||||
.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]
|
||||
|
|
|
@ -1030,8 +1030,7 @@ impl From<OauthClaimMapJoin> for DbValueOauthClaimMapJoinV1 {
|
|||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Oauth2Session {
|
||||
pub parent: Uuid,
|
||||
// pub expiry: Option<OffsetDateTime>,
|
||||
pub parent: Option<Uuid>,
|
||||
pub state: SessionState,
|
||||
pub issued_at: OffsetDateTime,
|
||||
pub rs_uuid: Uuid,
|
||||
|
|
|
@ -779,6 +779,8 @@ impl ValueSetOauth2Session {
|
|||
.map(SessionState::ExpiresAt)
|
||||
.unwrap_or(SessionState::NeverExpires);
|
||||
|
||||
let parent = Some(parent);
|
||||
|
||||
// Insert to the rs_filter.
|
||||
rs_filter |= rs_uuid.as_u128();
|
||||
Some((
|
||||
|
@ -833,6 +835,8 @@ impl ValueSetOauth2Session {
|
|||
|
||||
rs_filter |= rs_uuid.as_u128();
|
||||
|
||||
let parent = Some(parent);
|
||||
|
||||
Some((
|
||||
refer,
|
||||
Oauth2Session {
|
||||
|
@ -842,7 +846,59 @@ impl ValueSetOauth2Session {
|
|||
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();
|
||||
|
@ -1096,7 +1152,7 @@ impl ValueSetT for ValueSetOauth2Session {
|
|||
DbValueSetV2::Oauth2Session(
|
||||
self.map
|
||||
.iter()
|
||||
.map(|(u, m)| DbValueOauth2Session::V2 {
|
||||
.map(|(u, m)| DbValueOauth2Session::V3 {
|
||||
refer: *u,
|
||||
parent: m.parent,
|
||||
state: match &m.state {
|
||||
|
@ -1936,7 +1992,7 @@ mod tests {
|
|||
Oauth2Session {
|
||||
state: SessionState::NeverExpires,
|
||||
issued_at: OffsetDateTime::now_utc(),
|
||||
parent: Uuid::new_v4(),
|
||||
parent: Some(Uuid::new_v4()),
|
||||
rs_uuid: Uuid::new_v4(),
|
||||
},
|
||||
);
|
||||
|
@ -1966,7 +2022,7 @@ mod tests {
|
|||
Oauth2Session {
|
||||
state: SessionState::NeverExpires,
|
||||
issued_at: OffsetDateTime::now_utc(),
|
||||
parent: Uuid::new_v4(),
|
||||
parent: Some(Uuid::new_v4()),
|
||||
rs_uuid: Uuid::new_v4(),
|
||||
},
|
||||
);
|
||||
|
@ -1976,7 +2032,7 @@ mod tests {
|
|||
Oauth2Session {
|
||||
state: SessionState::RevokedAt(zero_cid.clone()),
|
||||
issued_at: OffsetDateTime::now_utc(),
|
||||
parent: Uuid::new_v4(),
|
||||
parent: Some(Uuid::new_v4()),
|
||||
rs_uuid: Uuid::new_v4(),
|
||||
},
|
||||
);
|
||||
|
@ -2001,7 +2057,7 @@ mod tests {
|
|||
Oauth2Session {
|
||||
state: SessionState::NeverExpires,
|
||||
issued_at: OffsetDateTime::now_utc(),
|
||||
parent: Uuid::new_v4(),
|
||||
parent: Some(Uuid::new_v4()),
|
||||
rs_uuid: Uuid::new_v4(),
|
||||
},
|
||||
);
|
||||
|
@ -2011,7 +2067,7 @@ mod tests {
|
|||
Oauth2Session {
|
||||
state: SessionState::RevokedAt(zero_cid.clone()),
|
||||
issued_at: OffsetDateTime::now_utc(),
|
||||
parent: Uuid::new_v4(),
|
||||
parent: Some(Uuid::new_v4()),
|
||||
rs_uuid: Uuid::new_v4(),
|
||||
},
|
||||
);
|
||||
|
@ -2039,7 +2095,7 @@ mod tests {
|
|||
Oauth2Session {
|
||||
state: SessionState::NeverExpires,
|
||||
issued_at: OffsetDateTime::now_utc(),
|
||||
parent: Uuid::new_v4(),
|
||||
parent: Some(Uuid::new_v4()),
|
||||
rs_uuid: Uuid::new_v4(),
|
||||
},
|
||||
);
|
||||
|
@ -2050,7 +2106,7 @@ mod tests {
|
|||
Oauth2Session {
|
||||
state: SessionState::RevokedAt(one_cid.clone()),
|
||||
issued_at: OffsetDateTime::now_utc(),
|
||||
parent: Uuid::new_v4(),
|
||||
parent: Some(Uuid::new_v4()),
|
||||
rs_uuid: Uuid::new_v4(),
|
||||
},
|
||||
),
|
||||
|
@ -2059,7 +2115,7 @@ mod tests {
|
|||
Oauth2Session {
|
||||
state: SessionState::RevokedAt(zero_cid.clone()),
|
||||
issued_at: OffsetDateTime::now_utc(),
|
||||
parent: Uuid::new_v4(),
|
||||
parent: Some(Uuid::new_v4()),
|
||||
rs_uuid: Uuid::new_v4(),
|
||||
},
|
||||
),
|
||||
|
@ -2093,7 +2149,7 @@ mod tests {
|
|||
Oauth2Session {
|
||||
state: SessionState::NeverExpires,
|
||||
issued_at: OffsetDateTime::now_utc(),
|
||||
parent: Uuid::new_v4(),
|
||||
parent: Some(Uuid::new_v4()),
|
||||
rs_uuid: Uuid::new_v4(),
|
||||
},
|
||||
);
|
||||
|
@ -2104,7 +2160,7 @@ mod tests {
|
|||
Oauth2Session {
|
||||
state: SessionState::RevokedAt(one_cid.clone()),
|
||||
issued_at: OffsetDateTime::now_utc(),
|
||||
parent: Uuid::new_v4(),
|
||||
parent: Some(Uuid::new_v4()),
|
||||
rs_uuid: Uuid::new_v4(),
|
||||
},
|
||||
),
|
||||
|
@ -2113,7 +2169,7 @@ mod tests {
|
|||
Oauth2Session {
|
||||
state: SessionState::RevokedAt(zero_cid.clone()),
|
||||
issued_at: OffsetDateTime::now_utc(),
|
||||
parent: Uuid::new_v4(),
|
||||
parent: Some(Uuid::new_v4()),
|
||||
rs_uuid: Uuid::new_v4(),
|
||||
},
|
||||
),
|
||||
|
@ -2151,7 +2207,7 @@ mod tests {
|
|||
Oauth2Session {
|
||||
state: SessionState::RevokedAt(zero_cid),
|
||||
issued_at: OffsetDateTime::now_utc(),
|
||||
parent: Uuid::new_v4(),
|
||||
parent: Some(Uuid::new_v4()),
|
||||
rs_uuid: Uuid::new_v4(),
|
||||
},
|
||||
),
|
||||
|
@ -2160,7 +2216,7 @@ mod tests {
|
|||
Oauth2Session {
|
||||
state: SessionState::RevokedAt(one_cid),
|
||||
issued_at: OffsetDateTime::now_utc(),
|
||||
parent: Uuid::new_v4(),
|
||||
parent: Some(Uuid::new_v4()),
|
||||
rs_uuid: Uuid::new_v4(),
|
||||
},
|
||||
),
|
||||
|
@ -2169,7 +2225,7 @@ mod tests {
|
|||
Oauth2Session {
|
||||
state: SessionState::RevokedAt(two_cid.clone()),
|
||||
issued_at: OffsetDateTime::now_utc(),
|
||||
parent: Uuid::new_v4(),
|
||||
parent: Some(Uuid::new_v4()),
|
||||
rs_uuid: Uuid::new_v4(),
|
||||
},
|
||||
),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#![deny(warnings)]
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::convert::TryFrom;
|
||||
use std::str::FromStr;
|
||||
|
||||
|
@ -346,7 +346,7 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
|
|||
|
||||
let response = client
|
||||
.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)
|
||||
.send()
|
||||
.await
|
||||
|
@ -415,6 +415,59 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
|
|||
|
||||
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
|
||||
let res = rsclient
|
||||
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
|
||||
|
|
Loading…
Reference in a new issue