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 crate::{ClientError, KanidmClient};
|
||||||
use kanidm_proto::constants::{
|
use kanidm_proto::constants::{
|
||||||
ATTR_DISPLAYNAME, ATTR_ES256_PRIVATE_KEY_DER, ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE,
|
ATTR_DISPLAYNAME, ATTR_ES256_PRIVATE_KEY_DER, ATTR_NAME,
|
||||||
ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT, ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE,
|
ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE, ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT,
|
||||||
ATTR_OAUTH2_PREFER_SHORT_USERNAME, ATTR_OAUTH2_RS_BASIC_SECRET, ATTR_OAUTH2_RS_NAME,
|
ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE, ATTR_OAUTH2_PREFER_SHORT_USERNAME,
|
||||||
ATTR_OAUTH2_RS_ORIGIN, ATTR_OAUTH2_RS_ORIGIN_LANDING, ATTR_OAUTH2_RS_TOKEN_KEY,
|
ATTR_OAUTH2_RS_BASIC_SECRET, ATTR_OAUTH2_RS_ORIGIN, ATTR_OAUTH2_RS_ORIGIN_LANDING,
|
||||||
ATTR_RS256_PRIVATE_KEY_DER,
|
ATTR_OAUTH2_RS_TOKEN_KEY, ATTR_RS256_PRIVATE_KEY_DER,
|
||||||
};
|
};
|
||||||
use kanidm_proto::internal::{ImageValue, Oauth2ClaimMapJoin};
|
use kanidm_proto::internal::{ImageValue, Oauth2ClaimMapJoin};
|
||||||
use kanidm_proto::v1::Entry;
|
use kanidm_proto::v1::Entry;
|
||||||
|
@ -27,7 +27,7 @@ impl KanidmClient {
|
||||||
let mut new_oauth2_rs = Entry::default();
|
let mut new_oauth2_rs = Entry::default();
|
||||||
new_oauth2_rs
|
new_oauth2_rs
|
||||||
.attrs
|
.attrs
|
||||||
.insert(ATTR_OAUTH2_RS_NAME.to_string(), vec![name.to_string()]);
|
.insert(ATTR_NAME.to_string(), vec![name.to_string()]);
|
||||||
new_oauth2_rs
|
new_oauth2_rs
|
||||||
.attrs
|
.attrs
|
||||||
.insert(ATTR_DISPLAYNAME.to_string(), vec![displayname.to_string()]);
|
.insert(ATTR_DISPLAYNAME.to_string(), vec![displayname.to_string()]);
|
||||||
|
@ -47,7 +47,7 @@ impl KanidmClient {
|
||||||
let mut new_oauth2_rs = Entry::default();
|
let mut new_oauth2_rs = Entry::default();
|
||||||
new_oauth2_rs
|
new_oauth2_rs
|
||||||
.attrs
|
.attrs
|
||||||
.insert(ATTR_OAUTH2_RS_NAME.to_string(), vec![name.to_string()]);
|
.insert(ATTR_NAME.to_string(), vec![name.to_string()]);
|
||||||
new_oauth2_rs
|
new_oauth2_rs
|
||||||
.attrs
|
.attrs
|
||||||
.insert(ATTR_DISPLAYNAME.to_string(), vec![displayname.to_string()]);
|
.insert(ATTR_DISPLAYNAME.to_string(), vec![displayname.to_string()]);
|
||||||
|
@ -91,7 +91,7 @@ impl KanidmClient {
|
||||||
if let Some(newname) = name {
|
if let Some(newname) = name {
|
||||||
update_oauth2_rs
|
update_oauth2_rs
|
||||||
.attrs
|
.attrs
|
||||||
.insert(ATTR_OAUTH2_RS_NAME.to_string(), vec![newname.to_string()]);
|
.insert(ATTR_NAME.to_string(), vec![newname.to_string()]);
|
||||||
}
|
}
|
||||||
if let Some(newdisplayname) = displayname {
|
if let Some(newdisplayname) = displayname {
|
||||||
update_oauth2_rs.attrs.insert(
|
update_oauth2_rs.attrs.insert(
|
||||||
|
|
|
@ -145,6 +145,7 @@ pub const ATTR_PRIVATE_COOKIE_KEY: &str = "private_cookie_key";
|
||||||
pub const ATTR_PRIVILEGE_EXPIRY: &str = "privilege_expiry";
|
pub const ATTR_PRIVILEGE_EXPIRY: &str = "privilege_expiry";
|
||||||
pub const ATTR_RADIUS_SECRET: &str = "radius_secret";
|
pub const ATTR_RADIUS_SECRET: &str = "radius_secret";
|
||||||
pub const ATTR_RECYCLED: &str = "recycled";
|
pub const ATTR_RECYCLED: &str = "recycled";
|
||||||
|
pub const ATTR_RECYCLEDDIRECTMEMBEROF: &str = "recycled_directmemberof";
|
||||||
pub const ATTR_REPLICATED: &str = "replicated";
|
pub const ATTR_REPLICATED: &str = "replicated";
|
||||||
pub const ATTR_RS256_PRIVATE_KEY_DER: &str = "rs256_private_key_der";
|
pub const ATTR_RS256_PRIVATE_KEY_DER: &str = "rs256_private_key_der";
|
||||||
pub const ATTR_SCOPE: &str = "scope";
|
pub const ATTR_SCOPE: &str = "scope";
|
||||||
|
|
|
@ -85,6 +85,10 @@ pub enum GrantTypeReq {
|
||||||
redirect_uri: Url,
|
redirect_uri: Url,
|
||||||
code_verifier: Option<String>,
|
code_verifier: Option<String>,
|
||||||
},
|
},
|
||||||
|
ClientCredentials {
|
||||||
|
#[serde_as(as = "Option<StringWithSeparator::<SpaceSeparator, String>>")]
|
||||||
|
scope: Option<BTreeSet<String>>,
|
||||||
|
},
|
||||||
RefreshToken {
|
RefreshToken {
|
||||||
refresh_token: String,
|
refresh_token: String,
|
||||||
#[serde_as(as = "Option<StringWithSeparator::<SpaceSeparator, String>>")]
|
#[serde_as(as = "Option<StringWithSeparator::<SpaceSeparator, String>>")]
|
||||||
|
|
|
@ -10,8 +10,8 @@ class OAuth2Rs(BaseModel):
|
||||||
classes: List[str]
|
classes: List[str]
|
||||||
displayname: str
|
displayname: str
|
||||||
es256_private_key_der: str
|
es256_private_key_der: str
|
||||||
|
name: str
|
||||||
oauth2_rs_basic_secret: str
|
oauth2_rs_basic_secret: str
|
||||||
oauth2_rs_name: str
|
|
||||||
oauth2_rs_origin: str
|
oauth2_rs_origin: str
|
||||||
oauth2_rs_token_key: str
|
oauth2_rs_token_key: str
|
||||||
oauth2_rs_sup_scope_map: List[str]
|
oauth2_rs_sup_scope_map: List[str]
|
||||||
|
@ -27,8 +27,8 @@ class RawOAuth2Rs(BaseModel):
|
||||||
required_fields = (
|
required_fields = (
|
||||||
"displayname",
|
"displayname",
|
||||||
"es256_private_key_der",
|
"es256_private_key_der",
|
||||||
|
"name",
|
||||||
"oauth2_rs_basic_secret",
|
"oauth2_rs_basic_secret",
|
||||||
"oauth2_rs_name",
|
|
||||||
"oauth2_rs_origin",
|
"oauth2_rs_origin",
|
||||||
"oauth2_rs_token_key",
|
"oauth2_rs_token_key",
|
||||||
)
|
)
|
||||||
|
@ -42,8 +42,8 @@ class RawOAuth2Rs(BaseModel):
|
||||||
classes=self.attrs["class"],
|
classes=self.attrs["class"],
|
||||||
displayname=self.attrs["displayname"][0],
|
displayname=self.attrs["displayname"][0],
|
||||||
es256_private_key_der=self.attrs["es256_private_key_der"][0],
|
es256_private_key_der=self.attrs["es256_private_key_der"][0],
|
||||||
|
name=self.attrs["name"][0],
|
||||||
oauth2_rs_basic_secret=self.attrs["oauth2_rs_basic_secret"][0],
|
oauth2_rs_basic_secret=self.attrs["oauth2_rs_basic_secret"][0],
|
||||||
oauth2_rs_name=self.attrs["oauth2_rs_name"][0],
|
|
||||||
oauth2_rs_origin=self.attrs["oauth2_rs_origin"][0],
|
oauth2_rs_origin=self.attrs["oauth2_rs_origin"][0],
|
||||||
oauth2_rs_token_key=self.attrs["oauth2_rs_token_key"][0],
|
oauth2_rs_token_key=self.attrs["oauth2_rs_token_key"][0],
|
||||||
oauth2_rs_sup_scope_map=self.attrs.get("oauth2_rs_sup_scope_map", []),
|
oauth2_rs_sup_scope_map=self.attrs.get("oauth2_rs_sup_scope_map", []),
|
||||||
|
|
|
@ -75,7 +75,7 @@ impl IntoResponse for HTTPOauth2Error {
|
||||||
pub(crate) fn oauth2_id(rs_name: &str) -> Filter<FilterInvalid> {
|
pub(crate) fn oauth2_id(rs_name: &str) -> Filter<FilterInvalid> {
|
||||||
filter_all!(f_and!([
|
filter_all!(f_and!([
|
||||||
f_eq(Attribute::Class, EntryClass::OAuth2ResourceServer.into()),
|
f_eq(Attribute::Class, EntryClass::OAuth2ResourceServer.into()),
|
||||||
f_eq(Attribute::OAuth2RsName, PartialValue::new_iname(rs_name))
|
f_eq(Attribute::Name, PartialValue::new_iname(rs_name))
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,7 @@ pub(crate) async fn oauth2_basic_post(
|
||||||
let classes = vec![
|
let classes = vec![
|
||||||
EntryClass::OAuth2ResourceServer.to_string(),
|
EntryClass::OAuth2ResourceServer.to_string(),
|
||||||
EntryClass::OAuth2ResourceServerBasic.to_string(),
|
EntryClass::OAuth2ResourceServerBasic.to_string(),
|
||||||
|
EntryClass::Account.to_string(),
|
||||||
EntryClass::Object.to_string(),
|
EntryClass::Object.to_string(),
|
||||||
];
|
];
|
||||||
json_rest_event_post(state, classes, obj, kopid, client_auth_info).await
|
json_rest_event_post(state, classes, obj, kopid, client_auth_info).await
|
||||||
|
@ -82,6 +83,7 @@ pub(crate) async fn oauth2_public_post(
|
||||||
let classes = vec![
|
let classes = vec![
|
||||||
EntryClass::OAuth2ResourceServer.to_string(),
|
EntryClass::OAuth2ResourceServer.to_string(),
|
||||||
EntryClass::OAuth2ResourceServerPublic.to_string(),
|
EntryClass::OAuth2ResourceServerPublic.to_string(),
|
||||||
|
EntryClass::Account.to_string(),
|
||||||
EntryClass::Object.to_string(),
|
EntryClass::Object.to_string(),
|
||||||
];
|
];
|
||||||
json_rest_event_post(state, classes, obj, kopid, client_auth_info).await
|
json_rest_event_post(state, classes, obj, kopid, client_auth_info).await
|
||||||
|
|
|
@ -617,7 +617,7 @@ async fn repl_acceptor(
|
||||||
};
|
};
|
||||||
|
|
||||||
// Setup a broadcast to control our tasks.
|
// Setup a broadcast to control our tasks.
|
||||||
let (task_tx, task_rx1) = broadcast::channel(2);
|
let (task_tx, task_rx1) = broadcast::channel(1);
|
||||||
// Note, we drop this task here since each task will re-subscribe. That way the
|
// Note, we drop this task here since each task will re-subscribe. That way the
|
||||||
// broadcast doesn't jam up because we aren't draining this task.
|
// broadcast doesn't jam up because we aren't draining this task.
|
||||||
drop(task_rx1);
|
drop(task_rx1);
|
||||||
|
|
|
@ -544,6 +544,7 @@ pub enum DbValueApiToken {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub enum DbValueOauth2Session {
|
pub enum DbValueOauth2Session {
|
||||||
V1 {
|
V1 {
|
||||||
|
@ -570,6 +571,18 @@ pub enum DbValueOauth2Session {
|
||||||
#[serde(rename = "r")]
|
#[serde(rename = "r")]
|
||||||
rs_uuid: Uuid,
|
rs_uuid: Uuid,
|
||||||
},
|
},
|
||||||
|
V3 {
|
||||||
|
#[serde(rename = "u")]
|
||||||
|
refer: Uuid,
|
||||||
|
#[serde(rename = "p")]
|
||||||
|
parent: Option<Uuid>,
|
||||||
|
#[serde(rename = "e")]
|
||||||
|
state: DbValueSessionStateV1,
|
||||||
|
#[serde(rename = "i")]
|
||||||
|
issued_at: String,
|
||||||
|
#[serde(rename = "r")]
|
||||||
|
rs_uuid: Uuid,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal representation of an image
|
// Internal representation of an image
|
||||||
|
|
|
@ -720,6 +720,108 @@ lazy_static! {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref IDM_ACP_OAUTH2_MANAGE_DL5: BuiltinAcp = BuiltinAcp {
|
||||||
|
classes: vec![
|
||||||
|
EntryClass::Object,
|
||||||
|
EntryClass::AccessControlProfile,
|
||||||
|
EntryClass::AccessControlCreate,
|
||||||
|
EntryClass::AccessControlDelete,
|
||||||
|
EntryClass::AccessControlModify,
|
||||||
|
EntryClass::AccessControlSearch
|
||||||
|
],
|
||||||
|
name: "idm_acp_hp_oauth2_manage_priv",
|
||||||
|
uuid: UUID_IDM_ACP_OAUTH2_MANAGE_V1,
|
||||||
|
description: "Builtin IDM Control for managing oauth2 resource server integrations.",
|
||||||
|
receiver: BuiltinAcpReceiver::Group(vec![UUID_IDM_OAUTH2_ADMINS]),
|
||||||
|
target: BuiltinAcpTarget::Filter(ProtoFilter::And(vec![
|
||||||
|
match_class_filter!(EntryClass::OAuth2ResourceServer),
|
||||||
|
FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone(),
|
||||||
|
])),
|
||||||
|
search_attrs: vec![
|
||||||
|
Attribute::Class,
|
||||||
|
Attribute::Description,
|
||||||
|
Attribute::DisplayName,
|
||||||
|
Attribute::Name,
|
||||||
|
Attribute::Spn,
|
||||||
|
Attribute::OAuth2Session,
|
||||||
|
Attribute::OAuth2RsOrigin,
|
||||||
|
Attribute::OAuth2RsOriginLanding,
|
||||||
|
Attribute::OAuth2RsScopeMap,
|
||||||
|
Attribute::OAuth2RsSupScopeMap,
|
||||||
|
Attribute::OAuth2RsBasicSecret,
|
||||||
|
Attribute::OAuth2RsTokenKey,
|
||||||
|
Attribute::Es256PrivateKeyDer,
|
||||||
|
Attribute::OAuth2AllowInsecureClientDisablePkce,
|
||||||
|
Attribute::Rs256PrivateKeyDer,
|
||||||
|
Attribute::OAuth2JwtLegacyCryptoEnable,
|
||||||
|
Attribute::OAuth2PreferShortUsername,
|
||||||
|
Attribute::OAuth2AllowLocalhostRedirect,
|
||||||
|
Attribute::OAuth2RsClaimMap,
|
||||||
|
Attribute::Image,
|
||||||
|
],
|
||||||
|
modify_removed_attrs: vec![
|
||||||
|
Attribute::Description,
|
||||||
|
Attribute::DisplayName,
|
||||||
|
Attribute::Name,
|
||||||
|
Attribute::OAuth2Session,
|
||||||
|
Attribute::OAuth2RsOrigin,
|
||||||
|
Attribute::OAuth2RsOriginLanding,
|
||||||
|
Attribute::OAuth2RsScopeMap,
|
||||||
|
Attribute::OAuth2RsSupScopeMap,
|
||||||
|
Attribute::OAuth2RsBasicSecret,
|
||||||
|
Attribute::OAuth2RsTokenKey,
|
||||||
|
Attribute::Es256PrivateKeyDer,
|
||||||
|
Attribute::OAuth2AllowInsecureClientDisablePkce,
|
||||||
|
Attribute::Rs256PrivateKeyDer,
|
||||||
|
Attribute::OAuth2JwtLegacyCryptoEnable,
|
||||||
|
Attribute::OAuth2PreferShortUsername,
|
||||||
|
Attribute::OAuth2AllowLocalhostRedirect,
|
||||||
|
Attribute::OAuth2RsClaimMap,
|
||||||
|
Attribute::Image,
|
||||||
|
],
|
||||||
|
modify_present_attrs: vec![
|
||||||
|
Attribute::Description,
|
||||||
|
Attribute::DisplayName,
|
||||||
|
Attribute::Name,
|
||||||
|
Attribute::OAuth2RsOrigin,
|
||||||
|
Attribute::OAuth2RsOriginLanding,
|
||||||
|
Attribute::OAuth2RsSupScopeMap,
|
||||||
|
Attribute::OAuth2RsScopeMap,
|
||||||
|
Attribute::OAuth2AllowInsecureClientDisablePkce,
|
||||||
|
Attribute::OAuth2JwtLegacyCryptoEnable,
|
||||||
|
Attribute::OAuth2PreferShortUsername,
|
||||||
|
Attribute::OAuth2AllowLocalhostRedirect,
|
||||||
|
Attribute::OAuth2RsClaimMap,
|
||||||
|
Attribute::Image,
|
||||||
|
],
|
||||||
|
create_attrs: vec![
|
||||||
|
Attribute::Class,
|
||||||
|
Attribute::Description,
|
||||||
|
Attribute::Name,
|
||||||
|
Attribute::OAuth2RsName,
|
||||||
|
Attribute::OAuth2RsOrigin,
|
||||||
|
Attribute::OAuth2RsOriginLanding,
|
||||||
|
Attribute::OAuth2RsSupScopeMap,
|
||||||
|
Attribute::OAuth2RsScopeMap,
|
||||||
|
Attribute::OAuth2AllowInsecureClientDisablePkce,
|
||||||
|
Attribute::OAuth2JwtLegacyCryptoEnable,
|
||||||
|
Attribute::OAuth2PreferShortUsername,
|
||||||
|
Attribute::OAuth2AllowLocalhostRedirect,
|
||||||
|
Attribute::OAuth2RsClaimMap,
|
||||||
|
Attribute::Image,
|
||||||
|
],
|
||||||
|
create_classes: vec![
|
||||||
|
EntryClass::Object,
|
||||||
|
EntryClass::Account,
|
||||||
|
EntryClass::OAuth2ResourceServer,
|
||||||
|
EntryClass::OAuth2ResourceServerBasic,
|
||||||
|
EntryClass::OAuth2ResourceServerPublic,
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref IDM_ACP_DOMAIN_ADMIN_V1: BuiltinAcp = BuiltinAcp {
|
pub static ref IDM_ACP_DOMAIN_ADMIN_V1: BuiltinAcp = BuiltinAcp {
|
||||||
classes: vec![
|
classes: vec![
|
||||||
|
|
|
@ -141,6 +141,7 @@ pub enum Attribute {
|
||||||
PrivateCookieKey,
|
PrivateCookieKey,
|
||||||
PrivilegeExpiry,
|
PrivilegeExpiry,
|
||||||
RadiusSecret,
|
RadiusSecret,
|
||||||
|
RecycledDirectMemberOf,
|
||||||
Replicated,
|
Replicated,
|
||||||
Rs256PrivateKeyDer,
|
Rs256PrivateKeyDer,
|
||||||
Scope,
|
Scope,
|
||||||
|
@ -329,6 +330,7 @@ impl TryFrom<String> for Attribute {
|
||||||
ATTR_PRIVATE_COOKIE_KEY => Attribute::PrivateCookieKey,
|
ATTR_PRIVATE_COOKIE_KEY => Attribute::PrivateCookieKey,
|
||||||
ATTR_PRIVILEGE_EXPIRY => Attribute::PrivilegeExpiry,
|
ATTR_PRIVILEGE_EXPIRY => Attribute::PrivilegeExpiry,
|
||||||
ATTR_RADIUS_SECRET => Attribute::RadiusSecret,
|
ATTR_RADIUS_SECRET => Attribute::RadiusSecret,
|
||||||
|
ATTR_RECYCLEDDIRECTMEMBEROF => Attribute::RecycledDirectMemberOf,
|
||||||
ATTR_REPLICATED => Attribute::Replicated,
|
ATTR_REPLICATED => Attribute::Replicated,
|
||||||
ATTR_RS256_PRIVATE_KEY_DER => Attribute::Rs256PrivateKeyDer,
|
ATTR_RS256_PRIVATE_KEY_DER => Attribute::Rs256PrivateKeyDer,
|
||||||
ATTR_SCOPE => Attribute::Scope,
|
ATTR_SCOPE => Attribute::Scope,
|
||||||
|
@ -492,6 +494,7 @@ impl From<Attribute> for &'static str {
|
||||||
Attribute::PrivateCookieKey => ATTR_PRIVATE_COOKIE_KEY,
|
Attribute::PrivateCookieKey => ATTR_PRIVATE_COOKIE_KEY,
|
||||||
Attribute::PrivilegeExpiry => ATTR_PRIVILEGE_EXPIRY,
|
Attribute::PrivilegeExpiry => ATTR_PRIVILEGE_EXPIRY,
|
||||||
Attribute::RadiusSecret => ATTR_RADIUS_SECRET,
|
Attribute::RadiusSecret => ATTR_RADIUS_SECRET,
|
||||||
|
Attribute::RecycledDirectMemberOf => ATTR_RECYCLEDDIRECTMEMBEROF,
|
||||||
Attribute::Replicated => ATTR_REPLICATED,
|
Attribute::Replicated => ATTR_REPLICATED,
|
||||||
Attribute::Rs256PrivateKeyDer => ATTR_RS256_PRIVATE_KEY_DER,
|
Attribute::Rs256PrivateKeyDer => ATTR_RS256_PRIVATE_KEY_DER,
|
||||||
Attribute::Scope => ATTR_SCOPE,
|
Attribute::Scope => ATTR_SCOPE,
|
||||||
|
@ -826,7 +829,6 @@ impl From<BuiltinAccount> for EntryInitNew {
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
|
||||||
/// Builtin System Admin account.
|
/// Builtin System Admin account.
|
||||||
pub static ref BUILTIN_ACCOUNT_ADMIN: BuiltinAccount = BuiltinAccount {
|
pub static ref BUILTIN_ACCOUNT_ADMIN: BuiltinAccount = BuiltinAccount {
|
||||||
account_type: AccountType::ServiceAccount,
|
account_type: AccountType::ServiceAccount,
|
||||||
|
@ -863,12 +865,18 @@ pub const UUID_TESTPERSON_2: Uuid = uuid!("538faac7-4d29-473b-a59d-23023ac19955"
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref E_TESTPERSON_1: EntryInitNew = entry_init!(
|
pub static ref E_TESTPERSON_1: EntryInitNew = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
|
(Attribute::DisplayName, Value::new_utf8s("Test Person 1")),
|
||||||
(Attribute::Uuid, Value::Uuid(UUID_TESTPERSON_1))
|
(Attribute::Uuid, Value::Uuid(UUID_TESTPERSON_1))
|
||||||
);
|
);
|
||||||
pub static ref E_TESTPERSON_2: EntryInitNew = entry_init!(
|
pub static ref E_TESTPERSON_2: EntryInitNew = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson2")),
|
(Attribute::Name, Value::new_iname("testperson2")),
|
||||||
|
(Attribute::DisplayName, Value::new_utf8s("Test Person 2")),
|
||||||
(Attribute::Uuid, Value::Uuid(UUID_TESTPERSON_2))
|
(Attribute::Uuid, Value::Uuid(UUID_TESTPERSON_2))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,16 +43,18 @@ pub const SYSTEM_INDEX_VERSION: i64 = 30;
|
||||||
*/
|
*/
|
||||||
pub type DomainVersion = u32;
|
pub type DomainVersion = u32;
|
||||||
|
|
||||||
|
pub const DOMAIN_LEVEL_0: DomainVersion = 0;
|
||||||
pub const DOMAIN_LEVEL_1: DomainVersion = 1;
|
pub const DOMAIN_LEVEL_1: DomainVersion = 1;
|
||||||
pub const DOMAIN_LEVEL_2: DomainVersion = 2;
|
pub const DOMAIN_LEVEL_2: DomainVersion = 2;
|
||||||
pub const DOMAIN_LEVEL_3: DomainVersion = 3;
|
pub const DOMAIN_LEVEL_3: DomainVersion = 3;
|
||||||
pub const DOMAIN_LEVEL_4: DomainVersion = 4;
|
pub const DOMAIN_LEVEL_4: DomainVersion = 4;
|
||||||
|
pub const DOMAIN_LEVEL_5: DomainVersion = 5;
|
||||||
// The minimum supported domain functional level
|
// The minimum supported domain functional level
|
||||||
pub const DOMAIN_MIN_LEVEL: DomainVersion = DOMAIN_LEVEL_2;
|
pub const DOMAIN_MIN_LEVEL: DomainVersion = DOMAIN_LEVEL_5;
|
||||||
// The target supported domain functional level
|
// The target supported domain functional level
|
||||||
pub const DOMAIN_TGT_LEVEL: DomainVersion = DOMAIN_LEVEL_4;
|
pub const DOMAIN_TGT_LEVEL: DomainVersion = DOMAIN_LEVEL_5;
|
||||||
// The maximum supported domain functional level
|
// The maximum supported domain functional level
|
||||||
pub const DOMAIN_MAX_LEVEL: DomainVersion = DOMAIN_LEVEL_4;
|
pub const DOMAIN_MAX_LEVEL: DomainVersion = DOMAIN_LEVEL_5;
|
||||||
|
|
||||||
// On test builds, define to 60 seconds
|
// On test builds, define to 60 seconds
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -65,15 +67,15 @@ pub const PURGE_FREQUENCY: u64 = 600;
|
||||||
/// In test, we limit the changelog to 10 minutes.
|
/// In test, we limit the changelog to 10 minutes.
|
||||||
pub const CHANGELOG_MAX_AGE: u64 = 600;
|
pub const CHANGELOG_MAX_AGE: u64 = 600;
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
/// A replica may be less than 1 day out of sync and catch up.
|
/// A replica may be up to 7 days out of sync before being denied updates.
|
||||||
pub const CHANGELOG_MAX_AGE: u64 = 86400;
|
pub const CHANGELOG_MAX_AGE: u64 = 7 * 86400;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
/// In test, we limit the recyclebin to 5 minutes.
|
/// In test, we limit the recyclebin to 5 minutes.
|
||||||
pub const RECYCLEBIN_MAX_AGE: u64 = 300;
|
pub const RECYCLEBIN_MAX_AGE: u64 = 300;
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
/// In production we allow 1 week
|
/// In production we allow 1 week
|
||||||
pub const RECYCLEBIN_MAX_AGE: u64 = 604_800;
|
pub const RECYCLEBIN_MAX_AGE: u64 = 7 * 86400;
|
||||||
|
|
||||||
// 5 minute auth session window.
|
// 5 minute auth session window.
|
||||||
pub const AUTH_SESSION_TIMEOUT: u64 = 300;
|
pub const AUTH_SESSION_TIMEOUT: u64 = 300;
|
||||||
|
|
|
@ -637,6 +637,32 @@ pub static ref SCHEMA_CLASS_PERSON: SchemaClass = SchemaClass {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub static ref SCHEMA_CLASS_PERSON_DL5: SchemaClass = SchemaClass {
|
||||||
|
uuid: UUID_SCHEMA_CLASS_PERSON,
|
||||||
|
name: EntryClass::Person.into(),
|
||||||
|
description: "Object representation of a person".to_string(),
|
||||||
|
|
||||||
|
sync_allowed: true,
|
||||||
|
systemmay: vec![
|
||||||
|
Attribute::PrimaryCredential.into(),
|
||||||
|
Attribute::PassKeys.into(),
|
||||||
|
Attribute::AttestedPasskeys.into(),
|
||||||
|
Attribute::CredentialUpdateIntentToken.into(),
|
||||||
|
Attribute::SshPublicKey.into(),
|
||||||
|
Attribute::RadiusSecret.into(),
|
||||||
|
Attribute::OAuth2ConsentScopeMap.into(),
|
||||||
|
Attribute::UserAuthTokenSession.into(),
|
||||||
|
Attribute::OAuth2Session.into(),
|
||||||
|
Attribute::Mail.into(),
|
||||||
|
Attribute::LegalName.into(),
|
||||||
|
],
|
||||||
|
systemmust: vec![
|
||||||
|
Attribute::IdVerificationEcKey.into()
|
||||||
|
],
|
||||||
|
systemexcludes: vec![EntryClass::ServiceAccount.into()],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
pub static ref SCHEMA_CLASS_ORGPERSON: SchemaClass = SchemaClass {
|
pub static ref SCHEMA_CLASS_ORGPERSON: SchemaClass = SchemaClass {
|
||||||
uuid: UUID_SCHEMA_CLASS_ORGPERSON,
|
uuid: UUID_SCHEMA_CLASS_ORGPERSON,
|
||||||
name: EntryClass::OrgPerson.into(),
|
name: EntryClass::OrgPerson.into(),
|
||||||
|
@ -725,7 +751,32 @@ pub static ref SCHEMA_CLASS_ACCOUNT: SchemaClass = SchemaClass {
|
||||||
],
|
],
|
||||||
systemsupplements: vec![
|
systemsupplements: vec![
|
||||||
EntryClass::Person.into(),
|
EntryClass::Person.into(),
|
||||||
EntryClass::ServiceAccount.into()],
|
EntryClass::ServiceAccount.into(),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
pub static ref SCHEMA_CLASS_ACCOUNT_DL5: SchemaClass = SchemaClass {
|
||||||
|
uuid: UUID_SCHEMA_CLASS_ACCOUNT,
|
||||||
|
name: EntryClass::Account.into(),
|
||||||
|
description: "Object representation of an account".to_string(),
|
||||||
|
|
||||||
|
sync_allowed: true,
|
||||||
|
systemmay: vec![
|
||||||
|
Attribute::AccountExpire.into(),
|
||||||
|
Attribute::AccountValidFrom.into(),
|
||||||
|
Attribute::NameHistory.into(),
|
||||||
|
],
|
||||||
|
systemmust: vec![
|
||||||
|
Attribute::DisplayName.into(),
|
||||||
|
Attribute::Name.into(),
|
||||||
|
Attribute::Spn.into()
|
||||||
|
],
|
||||||
|
systemsupplements: vec![
|
||||||
|
EntryClass::Person.into(),
|
||||||
|
EntryClass::ServiceAccount.into(),
|
||||||
|
EntryClass::OAuth2ResourceServer.into(),
|
||||||
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -745,6 +796,27 @@ pub static ref SCHEMA_CLASS_SERVICE_ACCOUNT: SchemaClass = SchemaClass {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub static ref SCHEMA_CLASS_SERVICE_ACCOUNT_DL5: SchemaClass = SchemaClass {
|
||||||
|
uuid: UUID_SCHEMA_CLASS_SERVICE_ACCOUNT,
|
||||||
|
name: EntryClass::ServiceAccount.into(),
|
||||||
|
description: "Object representation of service account".to_string(),
|
||||||
|
|
||||||
|
sync_allowed: true,
|
||||||
|
systemmay: vec![
|
||||||
|
Attribute::SshPublicKey.into(),
|
||||||
|
Attribute::UserAuthTokenSession.into(),
|
||||||
|
Attribute::OAuth2Session.into(),
|
||||||
|
Attribute::Description.into(),
|
||||||
|
|
||||||
|
Attribute::Mail.into(),
|
||||||
|
Attribute::PrimaryCredential.into(),
|
||||||
|
Attribute::JwsEs256PrivateKey.into(),
|
||||||
|
Attribute::ApiTokenSession.into(),
|
||||||
|
],
|
||||||
|
systemexcludes: vec![EntryClass::Person.into()],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
pub static ref SCHEMA_CLASS_SYNC_ACCOUNT: SchemaClass = SchemaClass {
|
pub static ref SCHEMA_CLASS_SYNC_ACCOUNT: SchemaClass = SchemaClass {
|
||||||
uuid: UUID_SCHEMA_CLASS_SYNC_ACCOUNT,
|
uuid: UUID_SCHEMA_CLASS_SYNC_ACCOUNT,
|
||||||
name: EntryClass::SyncAccount.into(),
|
name: EntryClass::SyncAccount.into(),
|
||||||
|
@ -877,6 +949,31 @@ pub static ref SCHEMA_CLASS_OAUTH2_RS_DL4: SchemaClass = SchemaClass {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub static ref SCHEMA_CLASS_OAUTH2_RS_DL5: SchemaClass = SchemaClass {
|
||||||
|
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS,
|
||||||
|
name: EntryClass::OAuth2ResourceServer.into(),
|
||||||
|
description: "The class representing a configured Oauth2 Resource Server".to_string(),
|
||||||
|
|
||||||
|
systemmay: vec![
|
||||||
|
Attribute::Description.into(),
|
||||||
|
Attribute::OAuth2RsScopeMap.into(),
|
||||||
|
Attribute::OAuth2RsSupScopeMap.into(),
|
||||||
|
Attribute::Rs256PrivateKeyDer.into(),
|
||||||
|
Attribute::OAuth2JwtLegacyCryptoEnable.into(),
|
||||||
|
Attribute::OAuth2PreferShortUsername.into(),
|
||||||
|
Attribute::OAuth2RsOriginLanding.into(),
|
||||||
|
Attribute::Image.into(),
|
||||||
|
Attribute::OAuth2RsClaimMap.into(),
|
||||||
|
Attribute::OAuth2Session.into(),
|
||||||
|
],
|
||||||
|
systemmust: vec![
|
||||||
|
Attribute::OAuth2RsOrigin.into(),
|
||||||
|
Attribute::OAuth2RsTokenKey.into(),
|
||||||
|
Attribute::Es256PrivateKeyDer.into(),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
pub static ref SCHEMA_CLASS_OAUTH2_RS_BASIC: SchemaClass = SchemaClass {
|
pub static ref SCHEMA_CLASS_OAUTH2_RS_BASIC: SchemaClass = SchemaClass {
|
||||||
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS_BASIC,
|
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS_BASIC,
|
||||||
name: EntryClass::OAuth2ResourceServerBasic.into(),
|
name: EntryClass::OAuth2ResourceServerBasic.into(),
|
||||||
|
@ -888,6 +985,19 @@ pub static ref SCHEMA_CLASS_OAUTH2_RS_BASIC: SchemaClass = SchemaClass {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub static ref SCHEMA_CLASS_OAUTH2_RS_BASIC_DL5: SchemaClass = SchemaClass {
|
||||||
|
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS_BASIC,
|
||||||
|
name: EntryClass::OAuth2ResourceServerBasic.into(),
|
||||||
|
description: "The class representing a configured Oauth2 Resource Server authenticated with http basic authentication".to_string(),
|
||||||
|
|
||||||
|
systemmay: vec![
|
||||||
|
Attribute::OAuth2AllowInsecureClientDisablePkce.into(),
|
||||||
|
],
|
||||||
|
systemmust: vec![ Attribute::OAuth2RsBasicSecret.into()],
|
||||||
|
systemexcludes: vec![ EntryClass::OAuth2ResourceServerPublic.into()],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
pub static ref SCHEMA_CLASS_OAUTH2_RS_PUBLIC: SchemaClass = SchemaClass {
|
pub static ref SCHEMA_CLASS_OAUTH2_RS_PUBLIC: SchemaClass = SchemaClass {
|
||||||
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS_PUBLIC,
|
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS_PUBLIC,
|
||||||
name: EntryClass::OAuth2ResourceServerPublic.into(),
|
name: EntryClass::OAuth2ResourceServerPublic.into(),
|
||||||
|
|
|
@ -274,6 +274,8 @@ pub const UUID_SCHEMA_ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT: Uuid =
|
||||||
uuid!("00000000-0000-0000-0000-ffff00000158");
|
uuid!("00000000-0000-0000-0000-ffff00000158");
|
||||||
pub const UUID_SCHEMA_ATTR_OAUTH2_RS_CLAIM_MAP: Uuid =
|
pub const UUID_SCHEMA_ATTR_OAUTH2_RS_CLAIM_MAP: Uuid =
|
||||||
uuid!("00000000-0000-0000-0000-ffff00000159");
|
uuid!("00000000-0000-0000-0000-ffff00000159");
|
||||||
|
pub const UUID_SCHEMA_ATTR_RECYCLEDDIRECTMEMBEROF: Uuid =
|
||||||
|
uuid!("00000000-0000-0000-0000-ffff00000160");
|
||||||
|
|
||||||
// System and domain infos
|
// System and domain infos
|
||||||
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
|
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
|
||||||
|
|
|
@ -1202,6 +1202,7 @@ impl Entry<EntryInvalid, EntryCommitted> {
|
||||||
self.remove_ava(Attribute::Class, &EntryClass::Recycled.into());
|
self.remove_ava(Attribute::Class, &EntryClass::Recycled.into());
|
||||||
self.remove_ava(Attribute::Class, &EntryClass::Conflict.into());
|
self.remove_ava(Attribute::Class, &EntryClass::Conflict.into());
|
||||||
self.purge_ava(Attribute::SourceUuid);
|
self.purge_ava(Attribute::SourceUuid);
|
||||||
|
self.purge_ava(Attribute::RecycledDirectMemberOf);
|
||||||
|
|
||||||
// Change state repl doesn't need this flag
|
// Change state repl doesn't need this flag
|
||||||
// self.valid.ecstate.revive(&self.valid.cid);
|
// self.valid.ecstate.revive(&self.valid.cid);
|
||||||
|
@ -2312,6 +2313,25 @@ where
|
||||||
&self.valid.ecstate
|
&self.valid.ecstate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determine if any attribute of this entry changed excluding the attribute named.
|
||||||
|
/// This allows for detection of entry changes unless the change was to a specific
|
||||||
|
/// attribute.
|
||||||
|
pub(crate) fn entry_changed_excluding_attribute(&self, attr: Attribute, cid: &Cid) -> bool {
|
||||||
|
use crate::repl::entry::State;
|
||||||
|
|
||||||
|
match self.get_changestate().current() {
|
||||||
|
State::Live { at: _, changes } => {
|
||||||
|
changes.iter().any(|(change_attr, change_id)| {
|
||||||
|
change_id >= cid &&
|
||||||
|
change_attr != attr.as_ref() &&
|
||||||
|
// This always changes, and could throw off other detections.
|
||||||
|
change_attr != Attribute::LastModifiedCid.as_ref()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
State::Tombstone { at } => at == cid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// ⚠️ - Invalidate an entry by resetting it's change state to time-zero. This entry
|
/// ⚠️ - Invalidate an entry by resetting it's change state to time-zero. This entry
|
||||||
/// can never be replicated after this.
|
/// can never be replicated after this.
|
||||||
/// This is a TEST ONLY method and will never be exposed in production.
|
/// This is a TEST ONLY method and will never be exposed in production.
|
||||||
|
|
|
@ -2008,8 +2008,8 @@ mod tests {
|
||||||
|
|
||||||
let e1 = entry_init!(
|
let e1 = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
|
||||||
(Attribute::Class, EntryClass::Account.to_value()),
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(
|
(
|
||||||
Attribute::Uuid,
|
Attribute::Uuid,
|
||||||
|
@ -2021,7 +2021,8 @@ mod tests {
|
||||||
|
|
||||||
let e2 = entry_init!(
|
let e2 = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value().clone()),
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson2")),
|
(Attribute::Name, Value::new_iname("testperson2")),
|
||||||
(
|
(
|
||||||
Attribute::Uuid,
|
Attribute::Uuid,
|
||||||
|
@ -2034,7 +2035,8 @@ mod tests {
|
||||||
// We need to add these and then push through the state machine.
|
// We need to add these and then push through the state machine.
|
||||||
let e_ts = entry_init!(
|
let e_ts = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value().clone()),
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson3")),
|
(Attribute::Name, Value::new_iname("testperson3")),
|
||||||
(
|
(
|
||||||
Attribute::Uuid,
|
Attribute::Uuid,
|
||||||
|
@ -2082,7 +2084,7 @@ mod tests {
|
||||||
let t_uuid = vs_refer![uuid!("a67c0c71-0b35-4218-a6b0-22d23d131d27")] as _;
|
let t_uuid = vs_refer![uuid!("a67c0c71-0b35-4218-a6b0-22d23d131d27")] as _;
|
||||||
let r_uuid = server_txn.resolve_valueset(&t_uuid);
|
let r_uuid = server_txn.resolve_valueset(&t_uuid);
|
||||||
debug!("{:?}", r_uuid);
|
debug!("{:?}", r_uuid);
|
||||||
assert!(r_uuid == Ok(vec!["testperson2".to_string()]));
|
assert!(r_uuid == Ok(vec!["testperson2@example.com".to_string()]));
|
||||||
|
|
||||||
// Resolve UUID non-exist
|
// Resolve UUID non-exist
|
||||||
let t_uuid_non = vs_refer![uuid!("b83e98f0-3d2e-41d2-9796-d8d993289c86")] as _;
|
let t_uuid_non = vs_refer![uuid!("b83e98f0-3d2e-41d2-9796-d8d993289c86")] as _;
|
||||||
|
|
|
@ -49,7 +49,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
||||||
.cloned()?;
|
.cloned()?;
|
||||||
|
|
||||||
let name = entry
|
let name = entry
|
||||||
.get_ava_single_iname(Attribute::OAuth2RsName)
|
.get_ava_single_iname(Attribute::Name)
|
||||||
.map(str::to_string)?;
|
.map(str::to_string)?;
|
||||||
|
|
||||||
Some(AppLink::Oauth2 {
|
Some(AppLink::Oauth2 {
|
||||||
|
@ -84,6 +84,7 @@ mod tests {
|
||||||
|
|
||||||
let e_rs: Entry<EntryInit, EntryNew> = entry_init!(
|
let e_rs: Entry<EntryInit, EntryNew> = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(
|
(
|
||||||
Attribute::Class,
|
Attribute::Class,
|
||||||
EntryClass::OAuth2ResourceServer.to_value()
|
EntryClass::OAuth2ResourceServer.to_value()
|
||||||
|
@ -92,10 +93,7 @@ mod tests {
|
||||||
Attribute::Class,
|
Attribute::Class,
|
||||||
EntryClass::OAuth2ResourceServerBasic.to_value()
|
EntryClass::OAuth2ResourceServerBasic.to_value()
|
||||||
),
|
),
|
||||||
(
|
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||||
Attribute::OAuth2RsName,
|
|
||||||
Value::new_iname("test_resource_server")
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
Attribute::DisplayName,
|
Attribute::DisplayName,
|
||||||
Value::new_utf8s("test_resource_server")
|
Value::new_utf8s("test_resource_server")
|
||||||
|
|
|
@ -154,6 +154,14 @@ pub(crate) enum Oauth2TokenType {
|
||||||
// We stash some details here for oidc.
|
// We stash some details here for oidc.
|
||||||
nonce: Option<String>,
|
nonce: Option<String>,
|
||||||
},
|
},
|
||||||
|
ClientAccess {
|
||||||
|
scopes: BTreeSet<String>,
|
||||||
|
session_id: Uuid,
|
||||||
|
uuid: Uuid,
|
||||||
|
expiry: time::OffsetDateTime,
|
||||||
|
iat: i64,
|
||||||
|
nbf: i64,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Oauth2TokenType {
|
impl fmt::Display for Oauth2TokenType {
|
||||||
|
@ -165,6 +173,9 @@ impl fmt::Display for Oauth2TokenType {
|
||||||
Oauth2TokenType::Refresh { session_id, .. } => {
|
Oauth2TokenType::Refresh { session_id, .. } => {
|
||||||
write!(f, "refresh_token ({session_id}) ")
|
write!(f, "refresh_token ({session_id}) ")
|
||||||
}
|
}
|
||||||
|
Oauth2TokenType::ClientAccess { session_id, .. } => {
|
||||||
|
write!(f, "client_access_token ({session_id})")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -275,6 +286,8 @@ pub struct Oauth2RS {
|
||||||
claim_map: BTreeMap<Uuid, Vec<(String, ClaimValue)>>,
|
claim_map: BTreeMap<Uuid, Vec<(String, ClaimValue)>>,
|
||||||
scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
|
scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
|
||||||
sup_scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
|
sup_scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
|
||||||
|
client_scopes: BTreeSet<String>,
|
||||||
|
client_sup_scopes: BTreeSet<String>,
|
||||||
// Our internal exchange encryption material for this rs.
|
// Our internal exchange encryption material for this rs.
|
||||||
token_fernet: Fernet,
|
token_fernet: Fernet,
|
||||||
jws_signer: Oauth2JwsSigner,
|
jws_signer: Oauth2JwsSigner,
|
||||||
|
@ -409,7 +422,7 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
|
||||||
|
|
||||||
// Now we know we can load the shared attrs.
|
// Now we know we can load the shared attrs.
|
||||||
let name = ent
|
let name = ent
|
||||||
.get_ava_single_iname(Attribute::OAuth2RsName)
|
.get_ava_single_iname(Attribute::Name)
|
||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
.ok_or(OperationError::InvalidValueState)?;
|
.ok_or(OperationError::InvalidValueState)?;
|
||||||
|
|
||||||
|
@ -449,6 +462,41 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// From our scope maps we can now determine what scopes would be granted to our
|
||||||
|
// client during a client credentials authentication.
|
||||||
|
let (client_scopes, client_sup_scopes) = if let Some(client_member_of) = ent.get_ava_refer(Attribute::MemberOf) {
|
||||||
|
let client_scopes =
|
||||||
|
scope_maps
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(u, m)| {
|
||||||
|
if client_member_of.contains(u) {
|
||||||
|
Some(m.iter())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
.cloned()
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
|
||||||
|
let client_sup_scopes = sup_scope_maps
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(u, m)| {
|
||||||
|
if client_member_of.contains(u) {
|
||||||
|
Some(m.iter())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
.cloned()
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
|
||||||
|
(client_scopes, client_sup_scopes)
|
||||||
|
} else {
|
||||||
|
(BTreeSet::default(), BTreeSet::default())
|
||||||
|
};
|
||||||
|
|
||||||
let e_claim_maps = ent
|
let e_claim_maps = ent
|
||||||
.get_ava_set(Attribute::OAuth2RsClaimMap)
|
.get_ava_set(Attribute::OAuth2RsClaimMap)
|
||||||
.and_then(|vs| vs.as_oauthclaim_map());
|
.and_then(|vs| vs.as_oauthclaim_map());
|
||||||
|
@ -573,6 +621,8 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
|
||||||
origin_https,
|
origin_https,
|
||||||
scope_maps,
|
scope_maps,
|
||||||
sup_scope_maps,
|
sup_scope_maps,
|
||||||
|
client_scopes,
|
||||||
|
client_sup_scopes,
|
||||||
claim_map,
|
claim_map,
|
||||||
token_fernet,
|
token_fernet,
|
||||||
jws_signer,
|
jws_signer,
|
||||||
|
@ -641,7 +691,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
.token_fernet
|
.token_fernet
|
||||||
.decrypt(&revoke_req.token)
|
.decrypt(&revoke_req.token)
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
admin_error!("Failed to decrypt token introspection request");
|
admin_error!("Failed to decrypt token revoke request");
|
||||||
Oauth2Error::InvalidRequest
|
Oauth2Error::InvalidRequest
|
||||||
})
|
})
|
||||||
.and_then(|data| {
|
.and_then(|data| {
|
||||||
|
@ -660,6 +710,12 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
uuid,
|
uuid,
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
|
| Oauth2TokenType::ClientAccess {
|
||||||
|
session_id,
|
||||||
|
expiry,
|
||||||
|
uuid,
|
||||||
|
..
|
||||||
|
}
|
||||||
| Oauth2TokenType::Refresh {
|
| Oauth2TokenType::Refresh {
|
||||||
session_id,
|
session_id,
|
||||||
expiry,
|
expiry,
|
||||||
|
@ -738,11 +794,13 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
};
|
};
|
||||||
|
|
||||||
// check the secret.
|
// check the secret.
|
||||||
match &o2rs.type_ {
|
let client_authentication_valid = match &o2rs.type_ {
|
||||||
OauthRSType::Basic { authz_secret, .. } => {
|
OauthRSType::Basic { authz_secret, .. } => {
|
||||||
match secret {
|
match secret {
|
||||||
Some(secret) => {
|
Some(secret) => {
|
||||||
if authz_secret != &secret {
|
if authz_secret == &secret {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
security_info!("Invalid OAuth2 client_id secret");
|
security_info!("Invalid OAuth2 client_id secret");
|
||||||
return Err(Oauth2Error::AuthenticationRequired);
|
return Err(Oauth2Error::AuthenticationRequired);
|
||||||
}
|
}
|
||||||
|
@ -757,14 +815,10 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Relies on the token to be valid - no further action needed.
|
// Relies on the token to be valid - no further action needed.
|
||||||
OauthRSType::Public { .. } => {}
|
OauthRSType::Public { .. } => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// We are authenticated! Yay! Now we can actually check things ...
|
// We are authenticated! Yay! Now we can actually check things ...
|
||||||
|
|
||||||
// TODO: add refresh token grant type.
|
|
||||||
// If it's a refresh token grant, are the consent permissions the same?
|
|
||||||
|
|
||||||
match &token_req.grant_type {
|
match &token_req.grant_type {
|
||||||
GrantTypeReq::AuthorizationCode {
|
GrantTypeReq::AuthorizationCode {
|
||||||
code,
|
code,
|
||||||
|
@ -777,6 +831,16 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
code_verifier.as_deref(),
|
code_verifier.as_deref(),
|
||||||
ct,
|
ct,
|
||||||
),
|
),
|
||||||
|
GrantTypeReq::ClientCredentials { scope } => {
|
||||||
|
if client_authentication_valid {
|
||||||
|
self.check_oauth2_token_client_credentials(o2rs, scope.as_ref(), ct)
|
||||||
|
} else {
|
||||||
|
security_info!(
|
||||||
|
"Unable to proceed with client credentials grant unless client authentication is provided and valid"
|
||||||
|
);
|
||||||
|
Err(Oauth2Error::AuthenticationRequired)
|
||||||
|
}
|
||||||
|
}
|
||||||
GrantTypeReq::RefreshToken {
|
GrantTypeReq::RefreshToken {
|
||||||
refresh_token,
|
refresh_token,
|
||||||
scope,
|
scope,
|
||||||
|
@ -1011,8 +1075,8 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
match token {
|
match token {
|
||||||
Oauth2TokenType::Access { .. } => {
|
Oauth2TokenType::Access { .. } | Oauth2TokenType::ClientAccess { .. } => {
|
||||||
admin_error!("attempt to refresh with access token");
|
admin_error!("attempt to refresh with an access token");
|
||||||
Err(Oauth2Error::InvalidToken)
|
Err(Oauth2Error::InvalidToken)
|
||||||
}
|
}
|
||||||
Oauth2TokenType::Refresh {
|
Oauth2TokenType::Refresh {
|
||||||
|
@ -1035,7 +1099,13 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
// Check the session is still valid. This call checks the parent session
|
// Check the session is still valid. This call checks the parent session
|
||||||
// and the OAuth2 session.
|
// and the OAuth2 session.
|
||||||
let valid = self
|
let valid = self
|
||||||
.check_oauth2_account_uuid_valid(uuid, session_id, parent_session_id, iat, ct)
|
.check_oauth2_account_uuid_valid(
|
||||||
|
uuid,
|
||||||
|
session_id,
|
||||||
|
Some(parent_session_id),
|
||||||
|
iat,
|
||||||
|
ct,
|
||||||
|
)
|
||||||
.map_err(|_| admin_error!("Account is not valid"));
|
.map_err(|_| admin_error!("Account is not valid"));
|
||||||
|
|
||||||
let Ok(Some(entry)) = valid else {
|
let Ok(Some(entry)) = valid else {
|
||||||
|
@ -1117,6 +1187,111 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip_all)]
|
||||||
|
fn check_oauth2_token_client_credentials(
|
||||||
|
&mut self,
|
||||||
|
o2rs: &Oauth2RS,
|
||||||
|
req_scopes: Option<&BTreeSet<String>>,
|
||||||
|
ct: Duration,
|
||||||
|
) -> Result<AccessTokenResponse, Oauth2Error> {
|
||||||
|
let req_scopes = req_scopes.cloned().unwrap_or_default();
|
||||||
|
|
||||||
|
// Validate all request scopes have valid syntax.
|
||||||
|
validate_scopes(&req_scopes)?;
|
||||||
|
|
||||||
|
// Of these scopes, which do we have available?
|
||||||
|
let avail_scopes: Vec<String> = req_scopes
|
||||||
|
.intersection(&o2rs.client_scopes)
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if avail_scopes.len() != req_scopes.len() {
|
||||||
|
admin_warn!(
|
||||||
|
ident = %o2rs.name,
|
||||||
|
requested_scopes = ?req_scopes,
|
||||||
|
available_scopes = ?o2rs.client_scopes,
|
||||||
|
"Client does not have access to the requested scopes"
|
||||||
|
);
|
||||||
|
return Err(Oauth2Error::AccessDenied);
|
||||||
|
}
|
||||||
|
|
||||||
|
// == ready to build the access token ==
|
||||||
|
|
||||||
|
let granted_scopes = avail_scopes
|
||||||
|
.into_iter()
|
||||||
|
.chain(o2rs.client_sup_scopes.iter().cloned())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
|
||||||
|
let odt_ct = OffsetDateTime::UNIX_EPOCH + ct;
|
||||||
|
let iat = ct.as_secs() as i64;
|
||||||
|
let expiry = odt_ct + Duration::from_secs(OAUTH2_ACCESS_TOKEN_EXPIRY as u64);
|
||||||
|
let expires_in = OAUTH2_ACCESS_TOKEN_EXPIRY;
|
||||||
|
|
||||||
|
let session_id = Uuid::new_v4();
|
||||||
|
|
||||||
|
let scope = if granted_scopes.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(str_join(&granted_scopes))
|
||||||
|
};
|
||||||
|
|
||||||
|
let uuid = o2rs.uuid;
|
||||||
|
|
||||||
|
let access_token_raw = Oauth2TokenType::ClientAccess {
|
||||||
|
scopes: granted_scopes,
|
||||||
|
session_id,
|
||||||
|
uuid,
|
||||||
|
expiry,
|
||||||
|
iat,
|
||||||
|
nbf: iat,
|
||||||
|
};
|
||||||
|
|
||||||
|
let access_token_data = serde_json::to_vec(&access_token_raw).map_err(|e| {
|
||||||
|
admin_error!(err = ?e, "Unable to encode token data");
|
||||||
|
Oauth2Error::ServerError(OperationError::SerdeJsonError)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let access_token = o2rs
|
||||||
|
.token_fernet
|
||||||
|
.encrypt_at_time(&access_token_data, ct.as_secs());
|
||||||
|
|
||||||
|
// Write the session to the db
|
||||||
|
let session = Value::Oauth2Session(
|
||||||
|
session_id,
|
||||||
|
Oauth2Session {
|
||||||
|
parent: None,
|
||||||
|
state: SessionState::ExpiresAt(expiry),
|
||||||
|
issued_at: odt_ct,
|
||||||
|
rs_uuid: o2rs.uuid,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// We need to create this session on the o2rs
|
||||||
|
let modlist = ModifyList::new_list(vec![Modify::Present(
|
||||||
|
Attribute::OAuth2Session.into(),
|
||||||
|
session,
|
||||||
|
)]);
|
||||||
|
|
||||||
|
self.qs_write
|
||||||
|
.internal_modify(
|
||||||
|
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(uuid))),
|
||||||
|
&modlist,
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
admin_error!("Failed to persist OAuth2 session record {:?}", e);
|
||||||
|
Oauth2Error::ServerError(e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(AccessTokenResponse {
|
||||||
|
access_token,
|
||||||
|
token_type: "Bearer".to_string(),
|
||||||
|
expires_in,
|
||||||
|
refresh_token: None,
|
||||||
|
scope,
|
||||||
|
id_token: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn generate_access_token_response(
|
fn generate_access_token_response(
|
||||||
&mut self,
|
&mut self,
|
||||||
o2rs: &Oauth2RS,
|
o2rs: &Oauth2RS,
|
||||||
|
@ -1271,7 +1446,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
let session = Value::Oauth2Session(
|
let session = Value::Oauth2Session(
|
||||||
session_id,
|
session_id,
|
||||||
Oauth2Session {
|
Oauth2Session {
|
||||||
parent: parent_session_id,
|
parent: Some(parent_session_id),
|
||||||
state: SessionState::ExpiresAt(refresh_expiry),
|
state: SessionState::ExpiresAt(refresh_expiry),
|
||||||
issued_at: odt_ct,
|
issued_at: odt_ct,
|
||||||
rs_uuid: o2rs.uuid,
|
rs_uuid: o2rs.uuid,
|
||||||
|
@ -1338,7 +1513,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
o2rs.token_fernet
|
o2rs.token_fernet
|
||||||
.decrypt(token)
|
.decrypt(token)
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
admin_error!("Failed to decrypt token introspection request");
|
admin_error!("Failed to decrypt token reflection request");
|
||||||
OperationError::CryptographyError
|
OperationError::CryptographyError
|
||||||
})
|
})
|
||||||
.and_then(|data| {
|
.and_then(|data| {
|
||||||
|
@ -1495,25 +1670,8 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
||||||
return Err(Oauth2Error::InvalidRequest);
|
return Err(Oauth2Error::InvalidRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
let failed_scopes = req_scopes
|
// Validate all request scopes have valid syntax.
|
||||||
.iter()
|
validate_scopes(&req_scopes)?;
|
||||||
.filter(|&s| !OAUTHSCOPE_RE.is_match(s))
|
|
||||||
.cloned()
|
|
||||||
.collect::<Vec<String>>();
|
|
||||||
if !failed_scopes.is_empty() {
|
|
||||||
let requested_scopes_string = req_scopes
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(",");
|
|
||||||
admin_error!(
|
|
||||||
"Invalid OAuth2 request - requested scopes ({}) but ({}) failed to pass validation rules - all must match the regex {}",
|
|
||||||
requested_scopes_string,
|
|
||||||
failed_scopes.join(","),
|
|
||||||
OAUTHSCOPE_RE.as_str()
|
|
||||||
);
|
|
||||||
return Err(Oauth2Error::InvalidScope);
|
|
||||||
}
|
|
||||||
|
|
||||||
let uat_scopes: BTreeSet<String> = o2rs
|
let uat_scopes: BTreeSet<String> = o2rs
|
||||||
.scope_maps
|
.scope_maps
|
||||||
|
@ -1758,6 +1916,8 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
||||||
|
|
||||||
// We are authenticated! Yay! Now we can actually check things ...
|
// We are authenticated! Yay! Now we can actually check things ...
|
||||||
|
|
||||||
|
let prefer_short_username = o2rs.prefer_short_username;
|
||||||
|
|
||||||
let token: Oauth2TokenType = o2rs
|
let token: Oauth2TokenType = o2rs
|
||||||
.token_fernet
|
.token_fernet
|
||||||
.decrypt(&intr_req.token)
|
.decrypt(&intr_req.token)
|
||||||
|
@ -1793,13 +1953,19 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
||||||
|
|
||||||
// Is the user expired, or the OAuth2 session invalid?
|
// Is the user expired, or the OAuth2 session invalid?
|
||||||
let valid = self
|
let valid = self
|
||||||
.check_oauth2_account_uuid_valid(uuid, session_id, parent_session_id, iat, ct)
|
.check_oauth2_account_uuid_valid(
|
||||||
|
uuid,
|
||||||
|
session_id,
|
||||||
|
Some(parent_session_id),
|
||||||
|
iat,
|
||||||
|
ct,
|
||||||
|
)
|
||||||
.map_err(|_| admin_error!("Account is not valid"));
|
.map_err(|_| admin_error!("Account is not valid"));
|
||||||
|
|
||||||
let Ok(Some(entry)) = valid else {
|
let Ok(Some(entry)) = valid else {
|
||||||
security_info!(
|
security_info!(
|
||||||
?uuid,
|
?uuid,
|
||||||
"access token has no account not valid, returning inactive"
|
"access token account is not valid, returning inactive"
|
||||||
);
|
);
|
||||||
return Ok(AccessTokenIntrospectResponse::inactive());
|
return Ok(AccessTokenIntrospectResponse::inactive());
|
||||||
};
|
};
|
||||||
|
@ -1819,12 +1985,79 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
||||||
|
|
||||||
let exp = expiry.unix_timestamp();
|
let exp = expiry.unix_timestamp();
|
||||||
|
|
||||||
|
let preferred_username = if prefer_short_username {
|
||||||
|
Some(account.name.clone())
|
||||||
|
} else {
|
||||||
|
Some(account.spn.clone())
|
||||||
|
};
|
||||||
|
|
||||||
let token_type = Some("access_token".to_string());
|
let token_type = Some("access_token".to_string());
|
||||||
Ok(AccessTokenIntrospectResponse {
|
Ok(AccessTokenIntrospectResponse {
|
||||||
active: true,
|
active: true,
|
||||||
scope,
|
scope,
|
||||||
client_id: Some(client_id.clone()),
|
client_id: Some(client_id.clone()),
|
||||||
username: Some(account.spn),
|
username: preferred_username,
|
||||||
|
token_type,
|
||||||
|
iat: Some(iat),
|
||||||
|
exp: Some(exp),
|
||||||
|
nbf: Some(nbf),
|
||||||
|
sub: Some(uuid.to_string()),
|
||||||
|
aud: Some(client_id),
|
||||||
|
iss: None,
|
||||||
|
jti: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Oauth2TokenType::ClientAccess {
|
||||||
|
scopes,
|
||||||
|
session_id,
|
||||||
|
uuid,
|
||||||
|
expiry,
|
||||||
|
iat,
|
||||||
|
nbf,
|
||||||
|
} => {
|
||||||
|
// Has this token expired?
|
||||||
|
let odt_ct = OffsetDateTime::UNIX_EPOCH + ct;
|
||||||
|
if expiry <= odt_ct {
|
||||||
|
security_info!(?uuid, "access token has expired, returning inactive");
|
||||||
|
return Ok(AccessTokenIntrospectResponse::inactive());
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can't do the same validity check for the client as we do with an account
|
||||||
|
let valid = self
|
||||||
|
.check_oauth2_account_uuid_valid(uuid, session_id, None, iat, ct)
|
||||||
|
.map_err(|_| admin_error!("Account is not valid"));
|
||||||
|
|
||||||
|
let Ok(Some(entry)) = valid else {
|
||||||
|
security_info!(
|
||||||
|
?uuid,
|
||||||
|
"access token account is not valid, returning inactive"
|
||||||
|
);
|
||||||
|
return Ok(AccessTokenIntrospectResponse::inactive());
|
||||||
|
};
|
||||||
|
|
||||||
|
let scope = if scopes.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(str_join(&scopes))
|
||||||
|
};
|
||||||
|
|
||||||
|
let exp = expiry.unix_timestamp();
|
||||||
|
|
||||||
|
let token_type = Some("access_token".to_string());
|
||||||
|
|
||||||
|
let username = if prefer_short_username {
|
||||||
|
entry
|
||||||
|
.get_ava_single_iname(Attribute::Name)
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
} else {
|
||||||
|
entry.get_ava_single_proto_string(Attribute::Spn)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(AccessTokenIntrospectResponse {
|
||||||
|
active: true,
|
||||||
|
scope,
|
||||||
|
client_id: Some(client_id.clone()),
|
||||||
|
username,
|
||||||
token_type,
|
token_type,
|
||||||
iat: Some(iat),
|
iat: Some(iat),
|
||||||
exp: Some(exp),
|
exp: Some(exp),
|
||||||
|
@ -1897,7 +2130,13 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
||||||
|
|
||||||
// Is the user expired, or the OAuth2 session invalid?
|
// Is the user expired, or the OAuth2 session invalid?
|
||||||
let valid = self
|
let valid = self
|
||||||
.check_oauth2_account_uuid_valid(uuid, session_id, parent_session_id, iat, ct)
|
.check_oauth2_account_uuid_valid(
|
||||||
|
uuid,
|
||||||
|
session_id,
|
||||||
|
Some(parent_session_id),
|
||||||
|
iat,
|
||||||
|
ct,
|
||||||
|
)
|
||||||
.map_err(|_| admin_error!("Account is not valid"));
|
.map_err(|_| admin_error!("Account is not valid"));
|
||||||
|
|
||||||
let Ok(Some(entry)) = valid else {
|
let Ok(Some(entry)) = valid else {
|
||||||
|
@ -1942,6 +2181,10 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// https://openid.net/specs/openid-connect-basic-1_0.html#UserInfoErrorResponse
|
// https://openid.net/specs/openid-connect-basic-1_0.html#UserInfoErrorResponse
|
||||||
|
Oauth2TokenType::ClientAccess { .. } => {
|
||||||
|
warn!("OpenID userinfo introspection of client access tokens is not currently supported.");
|
||||||
|
Err(Oauth2Error::InvalidToken)
|
||||||
|
}
|
||||||
Oauth2TokenType::Refresh { .. } => Err(Oauth2Error::InvalidToken),
|
Oauth2TokenType::Refresh { .. } => Err(Oauth2Error::InvalidToken),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2252,6 +2495,30 @@ fn str_join(set: &BTreeSet<String>) -> String {
|
||||||
buf
|
buf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_scopes(req_scopes: &BTreeSet<String>) -> Result<(), Oauth2Error> {
|
||||||
|
let failed_scopes = req_scopes
|
||||||
|
.iter()
|
||||||
|
.filter(|&s| !OAUTHSCOPE_RE.is_match(s))
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
if !failed_scopes.is_empty() {
|
||||||
|
let requested_scopes_string = req_scopes
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(",");
|
||||||
|
admin_error!(
|
||||||
|
"Invalid OAuth2 request - requested scopes ({}) but ({}) failed to pass validation rules - all must match the regex {}",
|
||||||
|
requested_scopes_string,
|
||||||
|
failed_scopes.join(","),
|
||||||
|
OAUTHSCOPE_RE.as_str()
|
||||||
|
);
|
||||||
|
return Err(Oauth2Error::InvalidScope);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use base64::{engine::general_purpose, Engine as _};
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
|
@ -2284,6 +2551,8 @@ mod tests {
|
||||||
const UAT_EXPIRE: u64 = 5;
|
const UAT_EXPIRE: u64 = 5;
|
||||||
const TOKEN_EXPIRE: u64 = 900;
|
const TOKEN_EXPIRE: u64 = 900;
|
||||||
|
|
||||||
|
const UUID_TESTGROUP: Uuid = uuid!("a3028223-bf20-47d5-8b65-967b5d2bb3eb");
|
||||||
|
|
||||||
macro_rules! create_code_verifier {
|
macro_rules! create_code_verifier {
|
||||||
($key:expr) => {{
|
($key:expr) => {{
|
||||||
let code_verifier = $key.to_string();
|
let code_verifier = $key.to_string();
|
||||||
|
@ -2333,10 +2602,19 @@ mod tests {
|
||||||
) -> (String, UserAuthToken, Identity, Uuid) {
|
) -> (String, UserAuthToken, Identity, Uuid) {
|
||||||
let mut idms_prox_write = idms.proxy_write(ct).await;
|
let mut idms_prox_write = idms.proxy_write(ct).await;
|
||||||
|
|
||||||
let uuid = Uuid::new_v4();
|
let rs_uuid = Uuid::new_v4();
|
||||||
|
|
||||||
let e: Entry<EntryInit, EntryNew> = entry_init!(
|
let entry_group: Entry<EntryInit, EntryNew> = entry_init!(
|
||||||
|
(Attribute::Class, EntryClass::Group.to_value()),
|
||||||
|
(Attribute::Name, Value::new_iname("testgroup")),
|
||||||
|
(Attribute::Description, Value::new_utf8s("testgroup")),
|
||||||
|
(Attribute::Uuid, Value::Uuid(UUID_TESTGROUP)),
|
||||||
|
(Attribute::Member, Value::Refer(UUID_TESTPERSON_1),)
|
||||||
|
);
|
||||||
|
|
||||||
|
let entry_rs: Entry<EntryInit, EntryNew> = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(
|
(
|
||||||
Attribute::Class,
|
Attribute::Class,
|
||||||
EntryClass::OAuth2ResourceServer.to_value()
|
EntryClass::OAuth2ResourceServer.to_value()
|
||||||
|
@ -2345,11 +2623,8 @@ mod tests {
|
||||||
Attribute::Class,
|
Attribute::Class,
|
||||||
EntryClass::OAuth2ResourceServerBasic.to_value()
|
EntryClass::OAuth2ResourceServerBasic.to_value()
|
||||||
),
|
),
|
||||||
(Attribute::Uuid, Value::Uuid(uuid)),
|
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
||||||
(
|
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||||
Attribute::OAuth2RsName,
|
|
||||||
Value::new_iname("test_resource_server")
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
Attribute::DisplayName,
|
Attribute::DisplayName,
|
||||||
Value::new_utf8s("test_resource_server")
|
Value::new_utf8s("test_resource_server")
|
||||||
|
@ -2362,7 +2637,7 @@ mod tests {
|
||||||
(
|
(
|
||||||
Attribute::OAuth2RsScopeMap,
|
Attribute::OAuth2RsScopeMap,
|
||||||
Value::new_oauthscopemap(
|
Value::new_oauthscopemap(
|
||||||
UUID_SYSTEM_ADMINS,
|
UUID_TESTGROUP,
|
||||||
btreeset![OAUTH2_SCOPE_GROUPS.to_string()]
|
btreeset![OAUTH2_SCOPE_GROUPS.to_string()]
|
||||||
)
|
)
|
||||||
.expect("invalid oauthscope")
|
.expect("invalid oauthscope")
|
||||||
|
@ -2396,12 +2671,12 @@ mod tests {
|
||||||
Value::new_bool(prefer_short_username)
|
Value::new_bool(prefer_short_username)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
let ce = CreateEvent::new_internal(vec![e]);
|
let ce = CreateEvent::new_internal(vec![entry_rs, entry_group, E_TESTPERSON_1.clone()]);
|
||||||
assert!(idms_prox_write.qs_write.create(&ce).is_ok());
|
assert!(idms_prox_write.qs_write.create(&ce).is_ok());
|
||||||
|
|
||||||
let entry = idms_prox_write
|
let entry = idms_prox_write
|
||||||
.qs_write
|
.qs_write
|
||||||
.internal_search_uuid(uuid)
|
.internal_search_uuid(rs_uuid)
|
||||||
.expect("Failed to retrieve OAuth2 resource entry ");
|
.expect("Failed to retrieve OAuth2 resource entry ");
|
||||||
let secret = entry
|
let secret = entry
|
||||||
.get_ava_single_secret(Attribute::OAuth2RsBasicSecret)
|
.get_ava_single_secret(Attribute::OAuth2RsBasicSecret)
|
||||||
|
@ -2410,12 +2685,12 @@ mod tests {
|
||||||
|
|
||||||
// Setup the uat we'll be using - note for these tests they *require*
|
// Setup the uat we'll be using - note for these tests they *require*
|
||||||
// the parent session to be valid and present!
|
// the parent session to be valid and present!
|
||||||
|
|
||||||
let session_id = uuid::Uuid::new_v4();
|
let session_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
let account = idms_prox_write
|
let account = idms_prox_write
|
||||||
.target_to_account(UUID_ADMIN)
|
.target_to_account(UUID_TESTPERSON_1)
|
||||||
.expect("account must exist");
|
.expect("account must exist");
|
||||||
|
|
||||||
let uat = account
|
let uat = account
|
||||||
.to_userauthtoken(
|
.to_userauthtoken(
|
||||||
session_id,
|
session_id,
|
||||||
|
@ -2459,7 +2734,7 @@ mod tests {
|
||||||
idms_prox_write
|
idms_prox_write
|
||||||
.qs_write
|
.qs_write
|
||||||
.internal_modify(
|
.internal_modify(
|
||||||
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_ADMIN))),
|
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
|
||||||
&modlist,
|
&modlist,
|
||||||
)
|
)
|
||||||
.expect("Failed to modify user");
|
.expect("Failed to modify user");
|
||||||
|
@ -2470,7 +2745,7 @@ mod tests {
|
||||||
|
|
||||||
idms_prox_write.commit().expect("failed to commit");
|
idms_prox_write.commit().expect("failed to commit");
|
||||||
|
|
||||||
(secret, uat, ident, uuid)
|
(secret, uat, ident, rs_uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn setup_oauth2_resource_server_public(
|
async fn setup_oauth2_resource_server_public(
|
||||||
|
@ -2479,10 +2754,19 @@ mod tests {
|
||||||
) -> (UserAuthToken, Identity, Uuid) {
|
) -> (UserAuthToken, Identity, Uuid) {
|
||||||
let mut idms_prox_write = idms.proxy_write(ct).await;
|
let mut idms_prox_write = idms.proxy_write(ct).await;
|
||||||
|
|
||||||
let uuid = Uuid::new_v4();
|
let rs_uuid = Uuid::new_v4();
|
||||||
|
|
||||||
let e: Entry<EntryInit, EntryNew> = entry_init!(
|
let entry_group: Entry<EntryInit, EntryNew> = entry_init!(
|
||||||
|
(Attribute::Class, EntryClass::Group.to_value()),
|
||||||
|
(Attribute::Name, Value::new_iname("testgroup")),
|
||||||
|
(Attribute::Description, Value::new_utf8s("testgroup")),
|
||||||
|
(Attribute::Uuid, Value::Uuid(UUID_TESTGROUP)),
|
||||||
|
(Attribute::Member, Value::Refer(UUID_TESTPERSON_1),)
|
||||||
|
);
|
||||||
|
|
||||||
|
let entry_rs: Entry<EntryInit, EntryNew> = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(
|
(
|
||||||
Attribute::Class,
|
Attribute::Class,
|
||||||
EntryClass::OAuth2ResourceServer.to_value()
|
EntryClass::OAuth2ResourceServer.to_value()
|
||||||
|
@ -2491,11 +2775,8 @@ mod tests {
|
||||||
Attribute::Class,
|
Attribute::Class,
|
||||||
EntryClass::OAuth2ResourceServerPublic.to_value()
|
EntryClass::OAuth2ResourceServerPublic.to_value()
|
||||||
),
|
),
|
||||||
(Attribute::Uuid, Value::Uuid(uuid)),
|
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
||||||
(
|
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||||
Attribute::OAuth2RsName,
|
|
||||||
Value::new_iname("test_resource_server")
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
Attribute::DisplayName,
|
Attribute::DisplayName,
|
||||||
Value::new_utf8s("test_resource_server")
|
Value::new_utf8s("test_resource_server")
|
||||||
|
@ -2507,7 +2788,7 @@ mod tests {
|
||||||
// System admins
|
// System admins
|
||||||
(
|
(
|
||||||
Attribute::OAuth2RsScopeMap,
|
Attribute::OAuth2RsScopeMap,
|
||||||
Value::new_oauthscopemap(UUID_SYSTEM_ADMINS, btreeset!["groups".to_string()])
|
Value::new_oauthscopemap(UUID_TESTGROUP, btreeset!["groups".to_string()])
|
||||||
.expect("invalid oauthscope")
|
.expect("invalid oauthscope")
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
@ -2527,7 +2808,7 @@ mod tests {
|
||||||
.expect("invalid oauthscope")
|
.expect("invalid oauthscope")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
let ce = CreateEvent::new_internal(vec![e]);
|
let ce = CreateEvent::new_internal(vec![entry_rs, entry_group, E_TESTPERSON_1.clone()]);
|
||||||
assert!(idms_prox_write.qs_write.create(&ce).is_ok());
|
assert!(idms_prox_write.qs_write.create(&ce).is_ok());
|
||||||
|
|
||||||
// Setup the uat we'll be using - note for these tests they *require*
|
// Setup the uat we'll be using - note for these tests they *require*
|
||||||
|
@ -2536,7 +2817,7 @@ mod tests {
|
||||||
let session_id = uuid::Uuid::new_v4();
|
let session_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
let account = idms_prox_write
|
let account = idms_prox_write
|
||||||
.target_to_account(UUID_ADMIN)
|
.target_to_account(UUID_TESTPERSON_1)
|
||||||
.expect("account must exist");
|
.expect("account must exist");
|
||||||
let uat = account
|
let uat = account
|
||||||
.to_userauthtoken(
|
.to_userauthtoken(
|
||||||
|
@ -2581,7 +2862,7 @@ mod tests {
|
||||||
idms_prox_write
|
idms_prox_write
|
||||||
.qs_write
|
.qs_write
|
||||||
.internal_modify(
|
.internal_modify(
|
||||||
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_ADMIN))),
|
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
|
||||||
&modlist,
|
&modlist,
|
||||||
)
|
)
|
||||||
.expect("Failed to modify user");
|
.expect("Failed to modify user");
|
||||||
|
@ -2592,7 +2873,7 @@ mod tests {
|
||||||
|
|
||||||
idms_prox_write.commit().expect("failed to commit");
|
idms_prox_write.commit().expect("failed to commit");
|
||||||
|
|
||||||
(uat, ident, uuid)
|
(uat, ident, rs_uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn setup_idm_admin(idms: &IdmServer, ct: Duration) -> (UserAuthToken, Identity) {
|
async fn setup_idm_admin(idms: &IdmServer, ct: Duration) -> (UserAuthToken, Identity) {
|
||||||
|
@ -3233,7 +3514,7 @@ mod tests {
|
||||||
assert!(intr_response.active);
|
assert!(intr_response.active);
|
||||||
assert!(intr_response.scope.as_deref() == Some("openid supplement"));
|
assert!(intr_response.scope.as_deref() == Some("openid supplement"));
|
||||||
assert!(intr_response.client_id.as_deref() == Some("test_resource_server"));
|
assert!(intr_response.client_id.as_deref() == Some("test_resource_server"));
|
||||||
assert!(intr_response.username.as_deref() == Some("admin@example.com"));
|
assert!(intr_response.username.as_deref() == Some("testperson1@example.com"));
|
||||||
assert!(intr_response.token_type.as_deref() == Some("access_token"));
|
assert!(intr_response.token_type.as_deref() == Some("access_token"));
|
||||||
assert!(intr_response.iat == Some(ct.as_secs() as i64));
|
assert!(intr_response.iat == Some(ct.as_secs() as i64));
|
||||||
assert!(intr_response.nbf == Some(ct.as_secs() as i64));
|
assert!(intr_response.nbf == Some(ct.as_secs() as i64));
|
||||||
|
@ -3245,7 +3526,7 @@ mod tests {
|
||||||
// Expire the account, should cause introspect to return inactive.
|
// Expire the account, should cause introspect to return inactive.
|
||||||
let v_expire = Value::new_datetime_epoch(Duration::from_secs(TEST_CURRENT_TIME - 1));
|
let v_expire = Value::new_datetime_epoch(Duration::from_secs(TEST_CURRENT_TIME - 1));
|
||||||
let me_inv_m = ModifyEvent::new_internal_invalid(
|
let me_inv_m = ModifyEvent::new_internal_invalid(
|
||||||
filter!(f_eq(Attribute::Name, PartialValue::new_iname("admin"))),
|
filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
|
||||||
ModifyList::new_list(vec![Modify::Present(
|
ModifyList::new_list(vec![Modify::Present(
|
||||||
Attribute::AccountExpire.into(),
|
Attribute::AccountExpire.into(),
|
||||||
v_expire,
|
v_expire,
|
||||||
|
@ -3486,8 +3767,10 @@ mod tests {
|
||||||
.expect("Failed to access internals of the refresh token");
|
.expect("Failed to access internals of the refresh token");
|
||||||
|
|
||||||
let session_id = match reflected_token {
|
let session_id = match reflected_token {
|
||||||
Oauth2TokenType::Refresh { session_id, .. } => session_id,
|
|
||||||
Oauth2TokenType::Access { session_id, .. } => session_id,
|
Oauth2TokenType::Access { session_id, .. } => session_id,
|
||||||
|
Oauth2TokenType::Refresh { .. } | Oauth2TokenType::ClientAccess { .. } => {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(idms_prox_write.commit().is_ok());
|
assert!(idms_prox_write.commit().is_ok());
|
||||||
|
@ -3498,7 +3781,7 @@ mod tests {
|
||||||
// Check it is now there
|
// Check it is now there
|
||||||
let entry = idms_prox_write
|
let entry = idms_prox_write
|
||||||
.qs_write
|
.qs_write
|
||||||
.internal_search_uuid(UUID_ADMIN)
|
.internal_search_uuid(UUID_TESTPERSON_1)
|
||||||
.expect("failed");
|
.expect("failed");
|
||||||
let valid = entry
|
let valid = entry
|
||||||
.get_ava_as_oauth2session_map(Attribute::OAuth2Session)
|
.get_ava_as_oauth2session_map(Attribute::OAuth2Session)
|
||||||
|
@ -3509,7 +3792,7 @@ mod tests {
|
||||||
// Delete the resource server.
|
// Delete the resource server.
|
||||||
|
|
||||||
let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
|
let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
|
||||||
Attribute::OAuth2RsName,
|
Attribute::Name,
|
||||||
PartialValue::new_iname("test_resource_server")
|
PartialValue::new_iname("test_resource_server")
|
||||||
)));
|
)));
|
||||||
|
|
||||||
|
@ -3520,7 +3803,7 @@ mod tests {
|
||||||
// revoked.
|
// revoked.
|
||||||
let entry = idms_prox_write
|
let entry = idms_prox_write
|
||||||
.qs_write
|
.qs_write
|
||||||
.internal_search_uuid(UUID_ADMIN)
|
.internal_search_uuid(UUID_TESTPERSON_1)
|
||||||
.expect("failed");
|
.expect("failed");
|
||||||
let revoked = entry
|
let revoked = entry
|
||||||
.get_ava_as_oauth2session_map(Attribute::OAuth2Session)
|
.get_ava_as_oauth2session_map(Attribute::OAuth2Session)
|
||||||
|
@ -3953,7 +4236,7 @@ mod tests {
|
||||||
== Url::parse("https://idm.example.com/oauth2/openid/test_resource_server")
|
== Url::parse("https://idm.example.com/oauth2/openid/test_resource_server")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
);
|
);
|
||||||
assert!(oidc.sub == OidcSubject::U(UUID_ADMIN));
|
assert!(oidc.sub == OidcSubject::U(UUID_TESTPERSON_1));
|
||||||
assert!(oidc.aud == "test_resource_server");
|
assert!(oidc.aud == "test_resource_server");
|
||||||
assert!(oidc.iat == iat);
|
assert!(oidc.iat == iat);
|
||||||
assert!(oidc.nbf == Some(iat));
|
assert!(oidc.nbf == Some(iat));
|
||||||
|
@ -3967,8 +4250,8 @@ mod tests {
|
||||||
assert!(oidc.amr.is_none());
|
assert!(oidc.amr.is_none());
|
||||||
assert!(oidc.azp == Some("test_resource_server".to_string()));
|
assert!(oidc.azp == Some("test_resource_server".to_string()));
|
||||||
assert!(oidc.jti.is_none());
|
assert!(oidc.jti.is_none());
|
||||||
assert!(oidc.s_claims.name == Some("System Administrator".to_string()));
|
assert!(oidc.s_claims.name == Some("Test Person 1".to_string()));
|
||||||
assert!(oidc.s_claims.preferred_username == Some("admin@example.com".to_string()));
|
assert!(oidc.s_claims.preferred_username == Some("testperson1@example.com".to_string()));
|
||||||
assert!(
|
assert!(
|
||||||
oidc.s_claims.scopes == vec![OAUTH2_SCOPE_OPENID.to_string(), "supplement".to_string()]
|
oidc.s_claims.scopes == vec![OAUTH2_SCOPE_OPENID.to_string(), "supplement".to_string()]
|
||||||
);
|
);
|
||||||
|
@ -4119,7 +4402,7 @@ mod tests {
|
||||||
.expect("Failed to verify oidc");
|
.expect("Failed to verify oidc");
|
||||||
|
|
||||||
// Do we have the short username in the token claims?
|
// Do we have the short username in the token claims?
|
||||||
assert!(oidc.s_claims.preferred_username == Some("admin".to_string()));
|
assert!(oidc.s_claims.preferred_username == Some("testperson1".to_string()));
|
||||||
// Do the id_token details line up to the userinfo?
|
// Do the id_token details line up to the userinfo?
|
||||||
let userinfo = idms_prox_read
|
let userinfo = idms_prox_read
|
||||||
.oauth2_openid_userinfo("test_resource_server", &access_token, ct)
|
.oauth2_openid_userinfo("test_resource_server", &access_token, ct)
|
||||||
|
@ -4364,7 +4647,7 @@ mod tests {
|
||||||
.verify_exp(iat)
|
.verify_exp(iat)
|
||||||
.expect("Failed to verify oidc");
|
.expect("Failed to verify oidc");
|
||||||
|
|
||||||
assert!(oidc.sub == OidcSubject::U(UUID_ADMIN));
|
assert!(oidc.sub == OidcSubject::U(UUID_TESTPERSON_1));
|
||||||
|
|
||||||
assert!(idms_prox_write.commit().is_ok());
|
assert!(idms_prox_write.commit().is_ok());
|
||||||
}
|
}
|
||||||
|
@ -4439,7 +4722,7 @@ mod tests {
|
||||||
|
|
||||||
let me_extend_scopes = ModifyEvent::new_internal_invalid(
|
let me_extend_scopes = ModifyEvent::new_internal_invalid(
|
||||||
filter!(f_eq(
|
filter!(f_eq(
|
||||||
Attribute::OAuth2RsName,
|
Attribute::Name,
|
||||||
PartialValue::new_iname("test_resource_server")
|
PartialValue::new_iname("test_resource_server")
|
||||||
)),
|
)),
|
||||||
ModifyList::new_list(vec![Modify::Present(
|
ModifyList::new_list(vec![Modify::Present(
|
||||||
|
@ -4505,7 +4788,7 @@ mod tests {
|
||||||
|
|
||||||
let me_extend_scopes = ModifyEvent::new_internal_invalid(
|
let me_extend_scopes = ModifyEvent::new_internal_invalid(
|
||||||
filter!(f_eq(
|
filter!(f_eq(
|
||||||
Attribute::OAuth2RsName,
|
Attribute::Name,
|
||||||
PartialValue::new_iname("test_resource_server")
|
PartialValue::new_iname("test_resource_server")
|
||||||
)),
|
)),
|
||||||
ModifyList::new_list(vec![Modify::Present(
|
ModifyList::new_list(vec![Modify::Present(
|
||||||
|
@ -4617,7 +4900,7 @@ mod tests {
|
||||||
|
|
||||||
// Now trigger the delete of the RS
|
// Now trigger the delete of the RS
|
||||||
let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
|
let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
|
||||||
Attribute::OAuth2RsName,
|
Attribute::Name,
|
||||||
PartialValue::new_iname("test_resource_server")
|
PartialValue::new_iname("test_resource_server")
|
||||||
)));
|
)));
|
||||||
|
|
||||||
|
@ -4935,7 +5218,7 @@ mod tests {
|
||||||
|
|
||||||
let refresh_exp = match reflected_token {
|
let refresh_exp = match reflected_token {
|
||||||
Oauth2TokenType::Refresh { expiry, .. } => expiry.unix_timestamp(),
|
Oauth2TokenType::Refresh { expiry, .. } => expiry.unix_timestamp(),
|
||||||
Oauth2TokenType::Access { .. } => unreachable!(),
|
Oauth2TokenType::Access { .. } | Oauth2TokenType::ClientAccess { .. } => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
|
let token_req: AccessTokenRequest = GrantTypeReq::RefreshToken {
|
||||||
|
@ -5176,7 +5459,7 @@ mod tests {
|
||||||
|
|
||||||
let entry = idms_prox_write
|
let entry = idms_prox_write
|
||||||
.qs_write
|
.qs_write
|
||||||
.internal_search_uuid(UUID_ADMIN)
|
.internal_search_uuid(UUID_TESTPERSON_1)
|
||||||
.expect("failed");
|
.expect("failed");
|
||||||
let valid = entry
|
let valid = entry
|
||||||
.get_ava_as_oauth2session_map(Attribute::OAuth2Session)
|
.get_ava_as_oauth2session_map(Attribute::OAuth2Session)
|
||||||
|
@ -5295,7 +5578,7 @@ mod tests {
|
||||||
Attribute::OAuth2RsClaimMap.into(),
|
Attribute::OAuth2RsClaimMap.into(),
|
||||||
Value::OauthClaimValue(
|
Value::OauthClaimValue(
|
||||||
"custom_a".to_string(),
|
"custom_a".to_string(),
|
||||||
UUID_SYSTEM_ADMINS,
|
UUID_TESTGROUP,
|
||||||
btreeset!["value_a".to_string()],
|
btreeset!["value_a".to_string()],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -5320,7 +5603,7 @@ mod tests {
|
||||||
Attribute::OAuth2RsClaimMap.into(),
|
Attribute::OAuth2RsClaimMap.into(),
|
||||||
Value::OauthClaimValue(
|
Value::OauthClaimValue(
|
||||||
"custom_b".to_string(),
|
"custom_b".to_string(),
|
||||||
UUID_SYSTEM_ADMINS,
|
UUID_TESTGROUP,
|
||||||
btreeset!["value_a".to_string()],
|
btreeset!["value_a".to_string()],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -5434,7 +5717,7 @@ mod tests {
|
||||||
== Url::parse("https://idm.example.com/oauth2/openid/test_resource_server")
|
== Url::parse("https://idm.example.com/oauth2/openid/test_resource_server")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
);
|
);
|
||||||
assert!(oidc.sub == OidcSubject::U(UUID_ADMIN));
|
assert!(oidc.sub == OidcSubject::U(UUID_TESTPERSON_1));
|
||||||
assert!(oidc.aud == "test_resource_server");
|
assert!(oidc.aud == "test_resource_server");
|
||||||
assert!(oidc.iat == iat);
|
assert!(oidc.iat == iat);
|
||||||
assert!(oidc.nbf == Some(iat));
|
assert!(oidc.nbf == Some(iat));
|
||||||
|
@ -5448,8 +5731,8 @@ mod tests {
|
||||||
assert!(oidc.amr.is_none());
|
assert!(oidc.amr.is_none());
|
||||||
assert!(oidc.azp == Some("test_resource_server".to_string()));
|
assert!(oidc.azp == Some("test_resource_server".to_string()));
|
||||||
assert!(oidc.jti.is_none());
|
assert!(oidc.jti.is_none());
|
||||||
assert!(oidc.s_claims.name == Some("System Administrator".to_string()));
|
assert!(oidc.s_claims.name == Some("Test Person 1".to_string()));
|
||||||
assert!(oidc.s_claims.preferred_username == Some("admin@example.com".to_string()));
|
assert!(oidc.s_claims.preferred_username == Some("testperson1@example.com".to_string()));
|
||||||
assert!(
|
assert!(
|
||||||
oidc.s_claims.scopes == vec![OAUTH2_SCOPE_OPENID.to_string(), "supplement".to_string()]
|
oidc.s_claims.scopes == vec![OAUTH2_SCOPE_OPENID.to_string(), "supplement".to_string()]
|
||||||
);
|
);
|
||||||
|
@ -5498,7 +5781,7 @@ mod tests {
|
||||||
assert!(intr_response.active);
|
assert!(intr_response.active);
|
||||||
assert!(intr_response.scope.as_deref() == Some("openid supplement"));
|
assert!(intr_response.scope.as_deref() == Some("openid supplement"));
|
||||||
assert!(intr_response.client_id.as_deref() == Some("test_resource_server"));
|
assert!(intr_response.client_id.as_deref() == Some("test_resource_server"));
|
||||||
assert!(intr_response.username.as_deref() == Some("admin@example.com"));
|
assert!(intr_response.username.as_deref() == Some("testperson1@example.com"));
|
||||||
assert!(intr_response.token_type.as_deref() == Some("access_token"));
|
assert!(intr_response.token_type.as_deref() == Some("access_token"));
|
||||||
assert!(intr_response.iat == Some(ct.as_secs() as i64));
|
assert!(intr_response.iat == Some(ct.as_secs() as i64));
|
||||||
assert!(intr_response.nbf == Some(ct.as_secs() as i64));
|
assert!(intr_response.nbf == Some(ct.as_secs() as i64));
|
||||||
|
@ -5596,4 +5879,161 @@ mod tests {
|
||||||
|
|
||||||
assert!(idms_prox_write.commit().is_ok());
|
assert!(idms_prox_write.commit().is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[idm_test]
|
||||||
|
async fn test_idm_oauth2_basic_client_credentials_grant_valid(
|
||||||
|
idms: &IdmServer,
|
||||||
|
_idms_delayed: &mut IdmServerDelayed,
|
||||||
|
) {
|
||||||
|
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||||
|
let (secret, _uat, _ident, _) =
|
||||||
|
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
|
||||||
|
let client_authz =
|
||||||
|
Some(general_purpose::STANDARD.encode(format!("test_resource_server:{secret}")));
|
||||||
|
|
||||||
|
// scope: Some(btreeset!["invalid_scope".to_string()]),
|
||||||
|
let mut idms_prox_write = idms.proxy_write(ct).await;
|
||||||
|
|
||||||
|
let token_req = AccessTokenRequest {
|
||||||
|
grant_type: GrantTypeReq::ClientCredentials { scope: None },
|
||||||
|
client_id: Some("test_resource_server".to_string()),
|
||||||
|
client_secret: Some(secret),
|
||||||
|
};
|
||||||
|
|
||||||
|
let oauth2_token = idms_prox_write
|
||||||
|
.check_oauth2_token_exchange(None, &token_req, ct)
|
||||||
|
.expect("Failed to perform OAuth2 token exchange");
|
||||||
|
|
||||||
|
assert!(idms_prox_write.commit().is_ok());
|
||||||
|
|
||||||
|
// 🎉 We got a token! In the future we can then check introspection from this point.
|
||||||
|
assert!(oauth2_token.token_type == "Bearer");
|
||||||
|
|
||||||
|
// Check Oauth2 Token Introspection
|
||||||
|
let mut idms_prox_read = idms.proxy_read().await;
|
||||||
|
|
||||||
|
let intr_request = AccessTokenIntrospectRequest {
|
||||||
|
token: oauth2_token.access_token.clone(),
|
||||||
|
token_type_hint: None,
|
||||||
|
};
|
||||||
|
let intr_response = idms_prox_read
|
||||||
|
.check_oauth2_token_introspect(client_authz.as_deref().unwrap(), &intr_request, ct)
|
||||||
|
.expect("Failed to inspect token");
|
||||||
|
|
||||||
|
eprintln!("👉 {intr_response:?}");
|
||||||
|
assert!(intr_response.active);
|
||||||
|
assert_eq!(intr_response.scope.as_deref(), Some("supplement"));
|
||||||
|
assert_eq!(
|
||||||
|
intr_response.client_id.as_deref(),
|
||||||
|
Some("test_resource_server")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
intr_response.username.as_deref(),
|
||||||
|
Some("test_resource_server@example.com")
|
||||||
|
);
|
||||||
|
assert_eq!(intr_response.token_type.as_deref(), Some("access_token"));
|
||||||
|
assert_eq!(intr_response.iat, Some(ct.as_secs() as i64));
|
||||||
|
assert_eq!(intr_response.nbf, Some(ct.as_secs() as i64));
|
||||||
|
|
||||||
|
drop(idms_prox_read);
|
||||||
|
|
||||||
|
// Assert we can revoke.
|
||||||
|
let mut idms_prox_write = idms.proxy_write(ct).await;
|
||||||
|
let revoke_request = TokenRevokeRequest {
|
||||||
|
token: oauth2_token.access_token.clone(),
|
||||||
|
token_type_hint: None,
|
||||||
|
};
|
||||||
|
assert!(idms_prox_write
|
||||||
|
.oauth2_token_revoke(client_authz.as_deref().unwrap(), &revoke_request, ct,)
|
||||||
|
.is_ok());
|
||||||
|
assert!(idms_prox_write.commit().is_ok());
|
||||||
|
|
||||||
|
// Now must be invalid.
|
||||||
|
let ct = ct + GRACE_WINDOW;
|
||||||
|
let mut idms_prox_read = idms.proxy_read().await;
|
||||||
|
|
||||||
|
let intr_request = AccessTokenIntrospectRequest {
|
||||||
|
token: oauth2_token.access_token.clone(),
|
||||||
|
token_type_hint: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let intr_response = idms_prox_read
|
||||||
|
.check_oauth2_token_introspect(client_authz.as_deref().unwrap(), &intr_request, ct)
|
||||||
|
.expect("Failed to inspect token");
|
||||||
|
assert!(!intr_response.active);
|
||||||
|
|
||||||
|
drop(idms_prox_read);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[idm_test]
|
||||||
|
async fn test_idm_oauth2_basic_client_credentials_grant_invalid(
|
||||||
|
idms: &IdmServer,
|
||||||
|
_idms_delayed: &mut IdmServerDelayed,
|
||||||
|
) {
|
||||||
|
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||||
|
let (secret, _uat, _ident, _) =
|
||||||
|
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
|
||||||
|
|
||||||
|
let mut idms_prox_write = idms.proxy_write(ct).await;
|
||||||
|
|
||||||
|
// Public Client
|
||||||
|
let token_req = AccessTokenRequest {
|
||||||
|
grant_type: GrantTypeReq::ClientCredentials { scope: None },
|
||||||
|
client_id: Some("test_resource_server".to_string()),
|
||||||
|
client_secret: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
idms_prox_write
|
||||||
|
.check_oauth2_token_exchange(None, &token_req, ct)
|
||||||
|
.unwrap_err(),
|
||||||
|
Oauth2Error::AuthenticationRequired
|
||||||
|
);
|
||||||
|
|
||||||
|
// Incorrect Password
|
||||||
|
let token_req = AccessTokenRequest {
|
||||||
|
grant_type: GrantTypeReq::ClientCredentials { scope: None },
|
||||||
|
client_id: Some("test_resource_server".to_string()),
|
||||||
|
client_secret: Some("wrong password".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
idms_prox_write
|
||||||
|
.check_oauth2_token_exchange(None, &token_req, ct)
|
||||||
|
.unwrap_err(),
|
||||||
|
Oauth2Error::AuthenticationRequired
|
||||||
|
);
|
||||||
|
|
||||||
|
// Invalid scope
|
||||||
|
let scope = Some(btreeset!["💅".to_string()]);
|
||||||
|
let token_req = AccessTokenRequest {
|
||||||
|
grant_type: GrantTypeReq::ClientCredentials { scope },
|
||||||
|
client_id: Some("test_resource_server".to_string()),
|
||||||
|
client_secret: Some(secret.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
idms_prox_write
|
||||||
|
.check_oauth2_token_exchange(None, &token_req, ct)
|
||||||
|
.unwrap_err(),
|
||||||
|
Oauth2Error::InvalidScope
|
||||||
|
);
|
||||||
|
|
||||||
|
// Scopes we aren't a member-of
|
||||||
|
let scope = Some(btreeset!["invalid_scope".to_string()]);
|
||||||
|
let token_req = AccessTokenRequest {
|
||||||
|
grant_type: GrantTypeReq::ClientCredentials { scope },
|
||||||
|
client_id: Some("test_resource_server".to_string()),
|
||||||
|
client_secret: Some(secret.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
idms_prox_write
|
||||||
|
.check_oauth2_token_exchange(None, &token_req, ct)
|
||||||
|
.unwrap_err(),
|
||||||
|
Oauth2Error::AccessDenied
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(idms_prox_write.commit().is_ok());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -628,7 +628,7 @@ pub trait IdmServerTransaction<'a> {
|
||||||
&mut self,
|
&mut self,
|
||||||
uuid: Uuid,
|
uuid: Uuid,
|
||||||
session_id: Uuid,
|
session_id: Uuid,
|
||||||
parent_session_id: Uuid,
|
parent_session_id: Option<Uuid>,
|
||||||
iat: i64,
|
iat: i64,
|
||||||
ct: Duration,
|
ct: Duration,
|
||||||
) -> Result<Option<Arc<Entry<EntrySealed, EntryCommitted>>>, OperationError> {
|
) -> Result<Option<Arc<Entry<EntrySealed, EntryCommitted>>>, OperationError> {
|
||||||
|
@ -661,9 +661,6 @@ pub trait IdmServerTransaction<'a> {
|
||||||
let oauth2_session = entry
|
let oauth2_session = entry
|
||||||
.get_ava_as_oauth2session_map(Attribute::OAuth2Session)
|
.get_ava_as_oauth2session_map(Attribute::OAuth2Session)
|
||||||
.and_then(|sessions| sessions.get(&session_id));
|
.and_then(|sessions| sessions.get(&session_id));
|
||||||
let uat_session = entry
|
|
||||||
.get_ava_as_session_map(Attribute::UserAuthTokenSession)
|
|
||||||
.and_then(|sessions| sessions.get(&parent_session_id));
|
|
||||||
|
|
||||||
if let Some(oauth2_session) = oauth2_session {
|
if let Some(oauth2_session) = oauth2_session {
|
||||||
// We have the oauth2 session, lets check it.
|
// We have the oauth2 session, lets check it.
|
||||||
|
@ -674,10 +671,19 @@ pub trait IdmServerTransaction<'a> {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do we have a parent session? If yes, we need to enforce it's presence.
|
||||||
|
if let Some(parent_session_id) = parent_session_id {
|
||||||
|
let uat_session = entry
|
||||||
|
.get_ava_as_session_map(Attribute::UserAuthTokenSession)
|
||||||
|
.and_then(|sessions| sessions.get(&parent_session_id));
|
||||||
|
|
||||||
if let Some(uat_session) = uat_session {
|
if let Some(uat_session) = uat_session {
|
||||||
let parent_session_valid = !matches!(uat_session.state, SessionState::RevokedAt(_));
|
let parent_session_valid =
|
||||||
|
!matches!(uat_session.state, SessionState::RevokedAt(_));
|
||||||
if parent_session_valid {
|
if parent_session_valid {
|
||||||
security_info!("A valid parent and oauth2 session value exists for this token");
|
security_info!(
|
||||||
|
"A valid parent and oauth2 session value exists for this token"
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
security_info!(
|
security_info!(
|
||||||
"The parent oauth2 session associated to this token is revoked."
|
"The parent oauth2 session associated to this token is revoked."
|
||||||
|
@ -692,6 +698,8 @@ pub trait IdmServerTransaction<'a> {
|
||||||
security_info!("The token grace window has passed and no entry parent sessions exist. Assuming invalid.");
|
security_info!("The token grace window has passed and no entry parent sessions exist. Assuming invalid.");
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// If we don't have a parent session id, we are good to proceed.
|
||||||
} else if grace_valid {
|
} else if grace_valid {
|
||||||
security_info!("The token grace window is in effect. Assuming valid.");
|
security_info!("The token grace window is in effect. Assuming valid.");
|
||||||
} else {
|
} else {
|
||||||
|
@ -2332,16 +2340,24 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn init_admin_w_password(idms: &IdmServer, pw: &str) -> Result<Uuid, OperationError> {
|
async fn init_testperson_w_password(
|
||||||
|
idms: &IdmServer,
|
||||||
|
pw: &str,
|
||||||
|
) -> Result<Uuid, OperationError> {
|
||||||
let p = CryptoPolicy::minimum();
|
let p = CryptoPolicy::minimum();
|
||||||
let cred = Credential::new_password_only(&p, pw)?;
|
let cred = Credential::new_password_only(&p, pw)?;
|
||||||
let cred_id = cred.uuid;
|
let cred_id = cred.uuid;
|
||||||
let v_cred = Value::new_credential("primary", cred);
|
let v_cred = Value::new_credential("primary", cred);
|
||||||
let mut idms_write = idms.proxy_write(duration_from_epoch_now()).await;
|
let mut idms_write = idms.proxy_write(duration_from_epoch_now()).await;
|
||||||
|
|
||||||
|
idms_write
|
||||||
|
.qs_write
|
||||||
|
.internal_create(vec![E_TESTPERSON_1.clone()])
|
||||||
|
.expect("Failed to create test person");
|
||||||
|
|
||||||
// now modify and provide a primary credential.
|
// now modify and provide a primary credential.
|
||||||
let me_inv_m = ModifyEvent::new_internal_invalid(
|
let me_inv_m = ModifyEvent::new_internal_invalid(
|
||||||
filter!(f_eq(Attribute::Name, PartialValue::new_iname("admin"))),
|
filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
|
||||||
ModifyList::new_list(vec![Modify::Present(
|
ModifyList::new_list(vec![Modify::Present(
|
||||||
Attribute::PrimaryCredential.into(),
|
Attribute::PrimaryCredential.into(),
|
||||||
v_cred,
|
v_cred,
|
||||||
|
@ -2353,7 +2369,7 @@ mod tests {
|
||||||
idms_write.commit().map(|()| cred_id)
|
idms_write.commit().map(|()| cred_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn init_admin_authsession_sid(idms: &IdmServer, ct: Duration, name: &str) -> Uuid {
|
async fn init_authsession_sid(idms: &IdmServer, ct: Duration, name: &str) -> Uuid {
|
||||||
let mut idms_auth = idms.auth().await;
|
let mut idms_auth = idms.auth().await;
|
||||||
let admin_init = AuthEvent::named_init(name);
|
let admin_init = AuthEvent::named_init(name);
|
||||||
|
|
||||||
|
@ -2387,9 +2403,9 @@ mod tests {
|
||||||
sessionid
|
sessionid
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_admin_password(idms: &IdmServer, pw: &str) -> String {
|
async fn check_testperson_password(idms: &IdmServer, pw: &str) -> String {
|
||||||
let sid =
|
let sid =
|
||||||
init_admin_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "admin").await;
|
init_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "testperson1").await;
|
||||||
|
|
||||||
let mut idms_auth = idms.auth().await;
|
let mut idms_auth = idms.auth().await;
|
||||||
let anon_step = AuthEvent::cred_step_password(sid, pw);
|
let anon_step = AuthEvent::cred_step_password(sid, pw);
|
||||||
|
@ -2436,10 +2452,10 @@ mod tests {
|
||||||
|
|
||||||
#[idm_test]
|
#[idm_test]
|
||||||
async fn test_idm_simple_password_auth(idms: &IdmServer, idms_delayed: &mut IdmServerDelayed) {
|
async fn test_idm_simple_password_auth(idms: &IdmServer, idms_delayed: &mut IdmServerDelayed) {
|
||||||
init_admin_w_password(idms, TEST_PASSWORD)
|
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to setup admin account");
|
.expect("Failed to setup admin account");
|
||||||
check_admin_password(idms, TEST_PASSWORD).await;
|
check_testperson_password(idms, TEST_PASSWORD).await;
|
||||||
|
|
||||||
// Clear our the session record
|
// Clear our the session record
|
||||||
let da = idms_delayed.try_recv().expect("invalid");
|
let da = idms_delayed.try_recv().expect("invalid");
|
||||||
|
@ -2452,14 +2468,14 @@ mod tests {
|
||||||
idms: &IdmServer,
|
idms: &IdmServer,
|
||||||
idms_delayed: &mut IdmServerDelayed,
|
idms_delayed: &mut IdmServerDelayed,
|
||||||
) {
|
) {
|
||||||
init_admin_w_password(idms, TEST_PASSWORD)
|
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to setup admin account");
|
.expect("Failed to setup admin account");
|
||||||
|
|
||||||
let sid = init_admin_authsession_sid(
|
let sid = init_authsession_sid(
|
||||||
idms,
|
idms,
|
||||||
Duration::from_secs(TEST_CURRENT_TIME),
|
Duration::from_secs(TEST_CURRENT_TIME),
|
||||||
"admin@example.com",
|
"testperson1@example.com",
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -2513,11 +2529,11 @@ mod tests {
|
||||||
_idms_delayed: &IdmServerDelayed,
|
_idms_delayed: &IdmServerDelayed,
|
||||||
idms_audit: &mut IdmServerAudit,
|
idms_audit: &mut IdmServerAudit,
|
||||||
) {
|
) {
|
||||||
init_admin_w_password(idms, TEST_PASSWORD)
|
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to setup admin account");
|
.expect("Failed to setup admin account");
|
||||||
let sid =
|
let sid =
|
||||||
init_admin_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "admin").await;
|
init_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "testperson1").await;
|
||||||
let mut idms_auth = idms.auth().await;
|
let mut idms_auth = idms.auth().await;
|
||||||
let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD_INC);
|
let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD_INC);
|
||||||
|
|
||||||
|
@ -2588,7 +2604,13 @@ mod tests {
|
||||||
#[idm_test]
|
#[idm_test]
|
||||||
async fn test_idm_regenerate_radius_secret(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
|
async fn test_idm_regenerate_radius_secret(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
|
||||||
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await;
|
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await;
|
||||||
let rrse = RegenerateRadiusSecretEvent::new_internal(UUID_ADMIN);
|
|
||||||
|
idms_prox_write
|
||||||
|
.qs_write
|
||||||
|
.internal_create(vec![E_TESTPERSON_1.clone()])
|
||||||
|
.expect("unable to create test person");
|
||||||
|
|
||||||
|
let rrse = RegenerateRadiusSecretEvent::new_internal(UUID_TESTPERSON_1);
|
||||||
|
|
||||||
// Generates a new credential when none exists
|
// Generates a new credential when none exists
|
||||||
let r1 = idms_prox_write
|
let r1 = idms_prox_write
|
||||||
|
@ -2604,19 +2626,25 @@ mod tests {
|
||||||
#[idm_test]
|
#[idm_test]
|
||||||
async fn test_idm_radiusauthtoken(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
|
async fn test_idm_radiusauthtoken(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
|
||||||
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await;
|
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await;
|
||||||
let rrse = RegenerateRadiusSecretEvent::new_internal(UUID_ADMIN);
|
|
||||||
|
idms_prox_write
|
||||||
|
.qs_write
|
||||||
|
.internal_create(vec![E_TESTPERSON_1.clone()])
|
||||||
|
.expect("unable to create test person");
|
||||||
|
|
||||||
|
let rrse = RegenerateRadiusSecretEvent::new_internal(UUID_TESTPERSON_1);
|
||||||
let r1 = idms_prox_write
|
let r1 = idms_prox_write
|
||||||
.regenerate_radius_secret(&rrse)
|
.regenerate_radius_secret(&rrse)
|
||||||
.expect("Failed to reset radius credential 1");
|
.expect("Failed to reset radius credential 1");
|
||||||
idms_prox_write.commit().expect("failed to commit");
|
idms_prox_write.commit().expect("failed to commit");
|
||||||
|
|
||||||
let mut idms_prox_read = idms.proxy_read().await;
|
let mut idms_prox_read = idms.proxy_read().await;
|
||||||
let admin_entry = idms_prox_read
|
let person_entry = idms_prox_read
|
||||||
.qs_read
|
.qs_read
|
||||||
.internal_search_uuid(UUID_ADMIN)
|
.internal_search_uuid(UUID_TESTPERSON_1)
|
||||||
.expect("Can't access admin entry.");
|
.expect("Can't access admin entry.");
|
||||||
|
|
||||||
let rate = RadiusAuthTokenEvent::new_impersonate(admin_entry, UUID_ADMIN);
|
let rate = RadiusAuthTokenEvent::new_impersonate(person_entry, UUID_TESTPERSON_1);
|
||||||
let tok_r = idms_prox_read
|
let tok_r = idms_prox_read
|
||||||
.get_radiusauthtoken(&rate, duration_from_epoch_now())
|
.get_radiusauthtoken(&rate, duration_from_epoch_now())
|
||||||
.expect("Failed to generate radius auth token");
|
.expect("Failed to generate radius auth token");
|
||||||
|
@ -2780,9 +2808,15 @@ mod tests {
|
||||||
{
|
{
|
||||||
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await;
|
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await;
|
||||||
// now modify and provide a primary credential.
|
// now modify and provide a primary credential.
|
||||||
|
|
||||||
|
idms_prox_write
|
||||||
|
.qs_write
|
||||||
|
.internal_create(vec![E_TESTPERSON_1.clone()])
|
||||||
|
.expect("Failed to create test person");
|
||||||
|
|
||||||
let me_inv_m =
|
let me_inv_m =
|
||||||
ModifyEvent::new_internal_invalid(
|
ModifyEvent::new_internal_invalid(
|
||||||
filter!(f_eq(Attribute::Name, PartialValue::new_iname("admin"))),
|
filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
|
||||||
ModifyList::new_list(vec![Modify::Present(
|
ModifyList::new_list(vec![Modify::Present(
|
||||||
Attribute::PasswordImport.into(),
|
Attribute::PasswordImport.into(),
|
||||||
Value::from("{SSHA512}JwrSUHkI7FTAfHRVR6KoFlSN0E3dmaQWARjZ+/UsShYlENOqDtFVU77HJLLrY2MuSp0jve52+pwtdVl2QUAHukQ0XUf5LDtM")
|
Value::from("{SSHA512}JwrSUHkI7FTAfHRVR6KoFlSN0E3dmaQWARjZ+/UsShYlENOqDtFVU77HJLLrY2MuSp0jve52+pwtdVl2QUAHukQ0XUf5LDtM")
|
||||||
|
@ -2796,18 +2830,18 @@ mod tests {
|
||||||
idms_delayed.check_is_empty_or_panic();
|
idms_delayed.check_is_empty_or_panic();
|
||||||
|
|
||||||
let mut idms_prox_read = idms.proxy_read().await;
|
let mut idms_prox_read = idms.proxy_read().await;
|
||||||
let admin_entry = idms_prox_read
|
let person_entry = idms_prox_read
|
||||||
.qs_read
|
.qs_read
|
||||||
.internal_search_uuid(UUID_ADMIN)
|
.internal_search_uuid(UUID_TESTPERSON_1)
|
||||||
.expect("Can't access admin entry.");
|
.expect("Can't access admin entry.");
|
||||||
let cred_before = admin_entry
|
let cred_before = person_entry
|
||||||
.get_ava_single_credential(Attribute::PrimaryCredential)
|
.get_ava_single_credential(Attribute::PrimaryCredential)
|
||||||
.expect("No credential present")
|
.expect("No credential present")
|
||||||
.clone();
|
.clone();
|
||||||
drop(idms_prox_read);
|
drop(idms_prox_read);
|
||||||
|
|
||||||
// Do an auth, this will trigger the action to send.
|
// Do an auth, this will trigger the action to send.
|
||||||
check_admin_password(idms, "password").await;
|
check_testperson_password(idms, "password").await;
|
||||||
|
|
||||||
// ⚠️ We have to be careful here. Between these two actions, it's possible
|
// ⚠️ We have to be careful here. Between these two actions, it's possible
|
||||||
// that on the pw upgrade that the credential uuid changes. This immediately
|
// that on the pw upgrade that the credential uuid changes. This immediately
|
||||||
|
@ -2827,11 +2861,11 @@ mod tests {
|
||||||
assert!(Ok(true) == r);
|
assert!(Ok(true) == r);
|
||||||
|
|
||||||
let mut idms_prox_read = idms.proxy_read().await;
|
let mut idms_prox_read = idms.proxy_read().await;
|
||||||
let admin_entry = idms_prox_read
|
let person_entry = idms_prox_read
|
||||||
.qs_read
|
.qs_read
|
||||||
.internal_search_uuid(UUID_ADMIN)
|
.internal_search_uuid(UUID_TESTPERSON_1)
|
||||||
.expect("Can't access admin entry.");
|
.expect("Can't access admin entry.");
|
||||||
let cred_after = admin_entry
|
let cred_after = person_entry
|
||||||
.get_ava_single_credential(Attribute::PrimaryCredential)
|
.get_ava_single_credential(Attribute::PrimaryCredential)
|
||||||
.expect("No credential present")
|
.expect("No credential present")
|
||||||
.clone();
|
.clone();
|
||||||
|
@ -2840,7 +2874,7 @@ mod tests {
|
||||||
assert_eq!(cred_before.uuid, cred_after.uuid);
|
assert_eq!(cred_before.uuid, cred_after.uuid);
|
||||||
|
|
||||||
// Check the admin pw still matches
|
// Check the admin pw still matches
|
||||||
check_admin_password(idms, "password").await;
|
check_testperson_password(idms, "password").await;
|
||||||
// Clear the next auth session record
|
// Clear the next auth session record
|
||||||
let da = idms_delayed.try_recv().expect("invalid");
|
let da = idms_delayed.try_recv().expect("invalid");
|
||||||
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
|
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
|
||||||
|
@ -2909,7 +2943,7 @@ mod tests {
|
||||||
const TEST_EXPIRE_TIME: u64 = TEST_CURRENT_TIME + 120;
|
const TEST_EXPIRE_TIME: u64 = TEST_CURRENT_TIME + 120;
|
||||||
const TEST_AFTER_EXPIRY: u64 = TEST_CURRENT_TIME + 240;
|
const TEST_AFTER_EXPIRY: u64 = TEST_CURRENT_TIME + 240;
|
||||||
|
|
||||||
async fn set_admin_valid_time(idms: &IdmServer) {
|
async fn set_testperson_valid_time(idms: &IdmServer) {
|
||||||
let mut idms_write = idms.proxy_write(duration_from_epoch_now()).await;
|
let mut idms_write = idms.proxy_write(duration_from_epoch_now()).await;
|
||||||
|
|
||||||
let v_valid_from = Value::new_datetime_epoch(Duration::from_secs(TEST_VALID_FROM_TIME));
|
let v_valid_from = Value::new_datetime_epoch(Duration::from_secs(TEST_VALID_FROM_TIME));
|
||||||
|
@ -2917,7 +2951,7 @@ mod tests {
|
||||||
|
|
||||||
// now modify and provide a primary credential.
|
// now modify and provide a primary credential.
|
||||||
let me_inv_m = ModifyEvent::new_internal_invalid(
|
let me_inv_m = ModifyEvent::new_internal_invalid(
|
||||||
filter!(f_eq(Attribute::Name, PartialValue::new_iname("admin"))),
|
filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
|
||||||
ModifyList::new_list(vec![
|
ModifyList::new_list(vec![
|
||||||
Modify::Present(Attribute::AccountExpire.into(), v_expire),
|
Modify::Present(Attribute::AccountExpire.into(), v_expire),
|
||||||
Modify::Present(Attribute::AccountValidFrom.into(), v_valid_from),
|
Modify::Present(Attribute::AccountValidFrom.into(), v_valid_from),
|
||||||
|
@ -2936,12 +2970,12 @@ mod tests {
|
||||||
) {
|
) {
|
||||||
// Any account that is not yet valrid / expired can't auth.
|
// Any account that is not yet valrid / expired can't auth.
|
||||||
|
|
||||||
init_admin_w_password(idms, TEST_PASSWORD)
|
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to setup admin account");
|
.expect("Failed to setup admin account");
|
||||||
// Set the valid bounds high/low
|
// Set the valid bounds high/low
|
||||||
// TEST_VALID_FROM_TIME/TEST_EXPIRE_TIME
|
// TEST_VALID_FROM_TIME/TEST_EXPIRE_TIME
|
||||||
set_admin_valid_time(idms).await;
|
set_testperson_valid_time(idms).await;
|
||||||
|
|
||||||
let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME);
|
let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME);
|
||||||
let time_high = Duration::from_secs(TEST_AFTER_EXPIRY);
|
let time_high = Duration::from_secs(TEST_AFTER_EXPIRY);
|
||||||
|
@ -2996,10 +3030,10 @@ mod tests {
|
||||||
_idms_delayed: &mut IdmServerDelayed,
|
_idms_delayed: &mut IdmServerDelayed,
|
||||||
) {
|
) {
|
||||||
// Any account that is expired can't unix auth.
|
// Any account that is expired can't unix auth.
|
||||||
init_admin_w_password(idms, TEST_PASSWORD)
|
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to setup admin account");
|
.expect("Failed to setup admin account");
|
||||||
set_admin_valid_time(idms).await;
|
set_testperson_valid_time(idms).await;
|
||||||
|
|
||||||
let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME);
|
let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME);
|
||||||
let time_high = Duration::from_secs(TEST_AFTER_EXPIRY);
|
let time_high = Duration::from_secs(TEST_AFTER_EXPIRY);
|
||||||
|
@ -3007,7 +3041,7 @@ mod tests {
|
||||||
// make the admin a valid posix account
|
// make the admin a valid posix account
|
||||||
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await;
|
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await;
|
||||||
let me_posix = ModifyEvent::new_internal_invalid(
|
let me_posix = ModifyEvent::new_internal_invalid(
|
||||||
filter!(f_eq(Attribute::Name, PartialValue::new_iname("admin"))),
|
filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
|
||||||
ModifyList::new_list(vec![
|
ModifyList::new_list(vec![
|
||||||
Modify::Present(Attribute::Class.into(), EntryClass::PosixAccount.into()),
|
Modify::Present(Attribute::Class.into(), EntryClass::PosixAccount.into()),
|
||||||
Modify::Present(Attribute::GidNumber.into(), Value::new_uint32(2001)),
|
Modify::Present(Attribute::GidNumber.into(), Value::new_uint32(2001)),
|
||||||
|
@ -3015,14 +3049,14 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());
|
assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());
|
||||||
|
|
||||||
let pce = UnixPasswordChangeEvent::new_internal(UUID_ADMIN, TEST_PASSWORD);
|
let pce = UnixPasswordChangeEvent::new_internal(UUID_TESTPERSON_1, TEST_PASSWORD);
|
||||||
|
|
||||||
assert!(idms_prox_write.set_unix_account_password(&pce).is_ok());
|
assert!(idms_prox_write.set_unix_account_password(&pce).is_ok());
|
||||||
assert!(idms_prox_write.commit().is_ok());
|
assert!(idms_prox_write.commit().is_ok());
|
||||||
|
|
||||||
// Now check auth when the time is too high or too low.
|
// Now check auth when the time is too high or too low.
|
||||||
let mut idms_auth = idms.auth().await;
|
let mut idms_auth = idms.auth().await;
|
||||||
let uuae_good = UnixUserAuthEvent::new_internal(UUID_ADMIN, TEST_PASSWORD);
|
let uuae_good = UnixUserAuthEvent::new_internal(UUID_TESTPERSON_1, TEST_PASSWORD);
|
||||||
|
|
||||||
let a1 = idms_auth.auth_unix(&uuae_good, time_low).await;
|
let a1 = idms_auth.auth_unix(&uuae_good, time_low).await;
|
||||||
// Should this actually send an error with the details? Or just silently act as
|
// Should this actually send an error with the details? Or just silently act as
|
||||||
|
@ -3041,20 +3075,20 @@ mod tests {
|
||||||
idms_auth.commit().expect("Must not fail");
|
idms_auth.commit().expect("Must not fail");
|
||||||
// Also check the generated unix tokens are invalid.
|
// Also check the generated unix tokens are invalid.
|
||||||
let mut idms_prox_read = idms.proxy_read().await;
|
let mut idms_prox_read = idms.proxy_read().await;
|
||||||
let uute = UnixUserTokenEvent::new_internal(UUID_ADMIN);
|
let uute = UnixUserTokenEvent::new_internal(UUID_TESTPERSON_1);
|
||||||
|
|
||||||
let tok_r = idms_prox_read
|
let tok_r = idms_prox_read
|
||||||
.get_unixusertoken(&uute, time_low)
|
.get_unixusertoken(&uute, time_low)
|
||||||
.expect("Failed to generate unix user token");
|
.expect("Failed to generate unix user token");
|
||||||
|
|
||||||
assert!(tok_r.name == "admin");
|
assert!(tok_r.name == "testperson1");
|
||||||
assert!(!tok_r.valid);
|
assert!(!tok_r.valid);
|
||||||
|
|
||||||
let tok_r = idms_prox_read
|
let tok_r = idms_prox_read
|
||||||
.get_unixusertoken(&uute, time_high)
|
.get_unixusertoken(&uute, time_high)
|
||||||
.expect("Failed to generate unix user token");
|
.expect("Failed to generate unix user token");
|
||||||
|
|
||||||
assert!(tok_r.name == "admin");
|
assert!(tok_r.name == "testperson1");
|
||||||
assert!(!tok_r.valid);
|
assert!(!tok_r.valid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3065,16 +3099,16 @@ mod tests {
|
||||||
) {
|
) {
|
||||||
// Any account not valid/expiry should not return
|
// Any account not valid/expiry should not return
|
||||||
// a radius packet.
|
// a radius packet.
|
||||||
init_admin_w_password(idms, TEST_PASSWORD)
|
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to setup admin account");
|
.expect("Failed to setup admin account");
|
||||||
set_admin_valid_time(idms).await;
|
set_testperson_valid_time(idms).await;
|
||||||
|
|
||||||
let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME);
|
let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME);
|
||||||
let time_high = Duration::from_secs(TEST_AFTER_EXPIRY);
|
let time_high = Duration::from_secs(TEST_AFTER_EXPIRY);
|
||||||
|
|
||||||
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await;
|
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await;
|
||||||
let rrse = RegenerateRadiusSecretEvent::new_internal(UUID_ADMIN);
|
let rrse = RegenerateRadiusSecretEvent::new_internal(UUID_TESTPERSON_1);
|
||||||
let _r1 = idms_prox_write
|
let _r1 = idms_prox_write
|
||||||
.regenerate_radius_secret(&rrse)
|
.regenerate_radius_secret(&rrse)
|
||||||
.expect("Failed to reset radius credential 1");
|
.expect("Failed to reset radius credential 1");
|
||||||
|
@ -3110,13 +3144,13 @@ mod tests {
|
||||||
idms_delayed: &mut IdmServerDelayed,
|
idms_delayed: &mut IdmServerDelayed,
|
||||||
idms_audit: &mut IdmServerAudit,
|
idms_audit: &mut IdmServerAudit,
|
||||||
) {
|
) {
|
||||||
init_admin_w_password(idms, TEST_PASSWORD)
|
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to setup admin account");
|
.expect("Failed to setup admin account");
|
||||||
|
|
||||||
// Auth invalid, no softlock present.
|
// Auth invalid, no softlock present.
|
||||||
let sid =
|
let sid =
|
||||||
init_admin_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "admin").await;
|
init_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "testperson1").await;
|
||||||
let mut idms_auth = idms.auth().await;
|
let mut idms_auth = idms.auth().await;
|
||||||
let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD_INC);
|
let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD_INC);
|
||||||
|
|
||||||
|
@ -3163,7 +3197,7 @@ mod tests {
|
||||||
// aka Auth valid immediate, (ct < exp), autofail
|
// aka Auth valid immediate, (ct < exp), autofail
|
||||||
// aka Auth invalid immediate, (ct < exp), autofail
|
// aka Auth invalid immediate, (ct < exp), autofail
|
||||||
let mut idms_auth = idms.auth().await;
|
let mut idms_auth = idms.auth().await;
|
||||||
let admin_init = AuthEvent::named_init("admin");
|
let admin_init = AuthEvent::named_init("testperson1");
|
||||||
|
|
||||||
let r1 = idms_auth
|
let r1 = idms_auth
|
||||||
.auth(
|
.auth(
|
||||||
|
@ -3208,8 +3242,11 @@ mod tests {
|
||||||
// Tested in the softlock state machine.
|
// Tested in the softlock state machine.
|
||||||
|
|
||||||
// Auth valid once softlock pass, valid. Count remains.
|
// Auth valid once softlock pass, valid. Count remains.
|
||||||
let sid =
|
let sid = init_authsession_sid(
|
||||||
init_admin_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME + 2), "admin")
|
idms,
|
||||||
|
Duration::from_secs(TEST_CURRENT_TIME + 2),
|
||||||
|
"testperson1",
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let mut idms_auth = idms.auth().await;
|
let mut idms_auth = idms.auth().await;
|
||||||
|
@ -3269,17 +3306,17 @@ mod tests {
|
||||||
_idms_delayed: &mut IdmServerDelayed,
|
_idms_delayed: &mut IdmServerDelayed,
|
||||||
idms_audit: &mut IdmServerAudit,
|
idms_audit: &mut IdmServerAudit,
|
||||||
) {
|
) {
|
||||||
init_admin_w_password(idms, TEST_PASSWORD)
|
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to setup admin account");
|
.expect("Failed to setup admin account");
|
||||||
|
|
||||||
// Start an *early* auth session.
|
// Start an *early* auth session.
|
||||||
let sid_early =
|
let sid_early =
|
||||||
init_admin_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "admin").await;
|
init_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "testperson1").await;
|
||||||
|
|
||||||
// Start a second auth session
|
// Start a second auth session
|
||||||
let sid_later =
|
let sid_later =
|
||||||
init_admin_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "admin").await;
|
init_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "testperson1").await;
|
||||||
// Get the detail wrong in sid_later.
|
// Get the detail wrong in sid_later.
|
||||||
let mut idms_auth = idms.auth().await;
|
let mut idms_auth = idms.auth().await;
|
||||||
let anon_step = AuthEvent::cred_step_password(sid_later, TEST_PASSWORD_INC);
|
let anon_step = AuthEvent::cred_step_password(sid_later, TEST_PASSWORD_INC);
|
||||||
|
@ -3364,13 +3401,13 @@ mod tests {
|
||||||
idms: &IdmServer,
|
idms: &IdmServer,
|
||||||
_idms_delayed: &mut IdmServerDelayed,
|
_idms_delayed: &mut IdmServerDelayed,
|
||||||
) {
|
) {
|
||||||
init_admin_w_password(idms, TEST_PASSWORD)
|
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to setup admin account");
|
.expect("Failed to setup admin account");
|
||||||
// make the admin a valid posix account
|
// make the admin a valid posix account
|
||||||
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await;
|
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await;
|
||||||
let me_posix = ModifyEvent::new_internal_invalid(
|
let me_posix = ModifyEvent::new_internal_invalid(
|
||||||
filter!(f_eq(Attribute::Name, PartialValue::new_iname("admin"))),
|
filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
|
||||||
ModifyList::new_list(vec![
|
ModifyList::new_list(vec![
|
||||||
Modify::Present(Attribute::Class.into(), EntryClass::PosixAccount.into()),
|
Modify::Present(Attribute::Class.into(), EntryClass::PosixAccount.into()),
|
||||||
Modify::Present(Attribute::GidNumber.into(), Value::new_uint32(2001)),
|
Modify::Present(Attribute::GidNumber.into(), Value::new_uint32(2001)),
|
||||||
|
@ -3378,13 +3415,13 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());
|
assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());
|
||||||
|
|
||||||
let pce = UnixPasswordChangeEvent::new_internal(UUID_ADMIN, TEST_PASSWORD);
|
let pce = UnixPasswordChangeEvent::new_internal(UUID_TESTPERSON_1, TEST_PASSWORD);
|
||||||
assert!(idms_prox_write.set_unix_account_password(&pce).is_ok());
|
assert!(idms_prox_write.set_unix_account_password(&pce).is_ok());
|
||||||
assert!(idms_prox_write.commit().is_ok());
|
assert!(idms_prox_write.commit().is_ok());
|
||||||
|
|
||||||
let mut idms_auth = idms.auth().await;
|
let mut idms_auth = idms.auth().await;
|
||||||
let uuae_good = UnixUserAuthEvent::new_internal(UUID_ADMIN, TEST_PASSWORD);
|
let uuae_good = UnixUserAuthEvent::new_internal(UUID_TESTPERSON_1, TEST_PASSWORD);
|
||||||
let uuae_bad = UnixUserAuthEvent::new_internal(UUID_ADMIN, TEST_PASSWORD_INC);
|
let uuae_bad = UnixUserAuthEvent::new_internal(UUID_TESTPERSON_1, TEST_PASSWORD_INC);
|
||||||
|
|
||||||
let a2 = idms_auth
|
let a2 = idms_auth
|
||||||
.auth_unix(&uuae_bad, Duration::from_secs(TEST_CURRENT_TIME))
|
.auth_unix(&uuae_bad, Duration::from_secs(TEST_CURRENT_TIME))
|
||||||
|
@ -3420,10 +3457,10 @@ mod tests {
|
||||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||||
let expiry = ct + Duration::from_secs((DEFAULT_AUTH_SESSION_EXPIRY + 1).into());
|
let expiry = ct + Duration::from_secs((DEFAULT_AUTH_SESSION_EXPIRY + 1).into());
|
||||||
// Do an authenticate
|
// Do an authenticate
|
||||||
init_admin_w_password(idms, TEST_PASSWORD)
|
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to setup admin account");
|
.expect("Failed to setup admin account");
|
||||||
let token = check_admin_password(idms, TEST_PASSWORD).await;
|
let token = check_testperson_password(idms, TEST_PASSWORD).await;
|
||||||
|
|
||||||
// Clear out the queued session record
|
// Clear out the queued session record
|
||||||
let da = idms_delayed.try_recv().expect("invalid");
|
let da = idms_delayed.try_recv().expect("invalid");
|
||||||
|
@ -3460,7 +3497,7 @@ mod tests {
|
||||||
let session_b = Uuid::new_v4();
|
let session_b = Uuid::new_v4();
|
||||||
|
|
||||||
// We need to put the credential on the admin.
|
// We need to put the credential on the admin.
|
||||||
let cred_id = init_admin_w_password(idms, TEST_PASSWORD)
|
let cred_id = init_testperson_w_password(idms, TEST_PASSWORD)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to setup admin account");
|
.expect("Failed to setup admin account");
|
||||||
|
|
||||||
|
@ -3468,14 +3505,14 @@ mod tests {
|
||||||
let mut idms_prox_read = idms.proxy_read().await;
|
let mut idms_prox_read = idms.proxy_read().await;
|
||||||
let admin = idms_prox_read
|
let admin = idms_prox_read
|
||||||
.qs_read
|
.qs_read
|
||||||
.internal_search_uuid(UUID_ADMIN)
|
.internal_search_uuid(UUID_TESTPERSON_1)
|
||||||
.expect("failed");
|
.expect("failed");
|
||||||
let sessions = admin.get_ava_as_session_map(Attribute::UserAuthTokenSession);
|
let sessions = admin.get_ava_as_session_map(Attribute::UserAuthTokenSession);
|
||||||
assert!(sessions.is_none());
|
assert!(sessions.is_none());
|
||||||
drop(idms_prox_read);
|
drop(idms_prox_read);
|
||||||
|
|
||||||
let da = DelayedAction::AuthSessionRecord(AuthSessionRecord {
|
let da = DelayedAction::AuthSessionRecord(AuthSessionRecord {
|
||||||
target_uuid: UUID_ADMIN,
|
target_uuid: UUID_TESTPERSON_1,
|
||||||
session_id: session_a,
|
session_id: session_a,
|
||||||
cred_id,
|
cred_id,
|
||||||
label: "Test Session A".to_string(),
|
label: "Test Session A".to_string(),
|
||||||
|
@ -3492,7 +3529,7 @@ mod tests {
|
||||||
let mut idms_prox_read = idms.proxy_read().await;
|
let mut idms_prox_read = idms.proxy_read().await;
|
||||||
let admin = idms_prox_read
|
let admin = idms_prox_read
|
||||||
.qs_read
|
.qs_read
|
||||||
.internal_search_uuid(UUID_ADMIN)
|
.internal_search_uuid(UUID_TESTPERSON_1)
|
||||||
.expect("failed");
|
.expect("failed");
|
||||||
let sessions = admin
|
let sessions = admin
|
||||||
.get_ava_as_session_map(Attribute::UserAuthTokenSession)
|
.get_ava_as_session_map(Attribute::UserAuthTokenSession)
|
||||||
|
@ -3506,7 +3543,7 @@ mod tests {
|
||||||
// When we re-auth, this is what triggers the session revoke via the delayed action.
|
// When we re-auth, this is what triggers the session revoke via the delayed action.
|
||||||
|
|
||||||
let da = DelayedAction::AuthSessionRecord(AuthSessionRecord {
|
let da = DelayedAction::AuthSessionRecord(AuthSessionRecord {
|
||||||
target_uuid: UUID_ADMIN,
|
target_uuid: UUID_TESTPERSON_1,
|
||||||
session_id: session_b,
|
session_id: session_b,
|
||||||
cred_id,
|
cred_id,
|
||||||
label: "Test Session B".to_string(),
|
label: "Test Session B".to_string(),
|
||||||
|
@ -3522,7 +3559,7 @@ mod tests {
|
||||||
let mut idms_prox_read = idms.proxy_read().await;
|
let mut idms_prox_read = idms.proxy_read().await;
|
||||||
let admin = idms_prox_read
|
let admin = idms_prox_read
|
||||||
.qs_read
|
.qs_read
|
||||||
.internal_search_uuid(UUID_ADMIN)
|
.internal_search_uuid(UUID_TESTPERSON_1)
|
||||||
.expect("failed");
|
.expect("failed");
|
||||||
let sessions = admin
|
let sessions = admin
|
||||||
.get_ava_as_session_map(Attribute::UserAuthTokenSession)
|
.get_ava_as_session_map(Attribute::UserAuthTokenSession)
|
||||||
|
@ -3556,10 +3593,10 @@ mod tests {
|
||||||
assert!(post_grace < expiry);
|
assert!(post_grace < expiry);
|
||||||
|
|
||||||
// Do an authenticate
|
// Do an authenticate
|
||||||
init_admin_w_password(idms, TEST_PASSWORD)
|
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to setup admin account");
|
.expect("Failed to setup admin account");
|
||||||
let token = check_admin_password(idms, TEST_PASSWORD).await;
|
let token = check_testperson_password(idms, TEST_PASSWORD).await;
|
||||||
|
|
||||||
// Process the session info.
|
// Process the session info.
|
||||||
let da = idms_delayed.try_recv().expect("invalid");
|
let da = idms_delayed.try_recv().expect("invalid");
|
||||||
|
@ -3922,10 +3959,10 @@ mod tests {
|
||||||
) {
|
) {
|
||||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||||
|
|
||||||
init_admin_w_password(idms, TEST_PASSWORD)
|
init_testperson_w_password(idms, TEST_PASSWORD)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to setup admin account");
|
.expect("Failed to setup admin account");
|
||||||
let token = check_admin_password(idms, TEST_PASSWORD).await;
|
let token = check_testperson_password(idms, TEST_PASSWORD).await;
|
||||||
|
|
||||||
// Clear the session record
|
// Clear the session record
|
||||||
let da = idms_delayed.try_recv().expect("invalid");
|
let da = idms_delayed.try_recv().expect("invalid");
|
||||||
|
@ -3958,7 +3995,7 @@ mod tests {
|
||||||
assert!(idms_prox_write.qs_write.modify(&me_reset_tokens).is_ok());
|
assert!(idms_prox_write.qs_write.modify(&me_reset_tokens).is_ok());
|
||||||
assert!(idms_prox_write.commit().is_ok());
|
assert!(idms_prox_write.commit().is_ok());
|
||||||
// Check the old token is invalid, due to reload.
|
// Check the old token is invalid, due to reload.
|
||||||
let new_token = check_admin_password(idms, TEST_PASSWORD).await;
|
let new_token = check_testperson_password(idms, TEST_PASSWORD).await;
|
||||||
|
|
||||||
// Clear the session record
|
// Clear the session record
|
||||||
let da = idms_delayed.try_recv().expect("invalid");
|
let da = idms_delayed.try_recv().expect("invalid");
|
||||||
|
|
|
@ -500,6 +500,7 @@ mod tests {
|
||||||
fn test_pre_create_name_unique() {
|
fn test_pre_create_name_unique() {
|
||||||
let e: Entry<EntryInit, EntryNew> = entry_init!(
|
let e: Entry<EntryInit, EntryNew> = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson")),
|
(Attribute::Name, Value::new_iname("testperson")),
|
||||||
(Attribute::Description, Value::new_utf8s("testperson")),
|
(Attribute::Description, Value::new_utf8s("testperson")),
|
||||||
(Attribute::DisplayName, Value::new_utf8s("testperson"))
|
(Attribute::DisplayName, Value::new_utf8s("testperson"))
|
||||||
|
@ -524,6 +525,7 @@ mod tests {
|
||||||
fn test_pre_create_name_unique_2() {
|
fn test_pre_create_name_unique_2() {
|
||||||
let e: Entry<EntryInit, EntryNew> = entry_init!(
|
let e: Entry<EntryInit, EntryNew> = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson")),
|
(Attribute::Name, Value::new_iname("testperson")),
|
||||||
(Attribute::Description, Value::new_utf8s("testperson")),
|
(Attribute::Description, Value::new_utf8s("testperson")),
|
||||||
(Attribute::DisplayName, Value::new_utf8s("testperson"))
|
(Attribute::DisplayName, Value::new_utf8s("testperson"))
|
||||||
|
|
|
@ -333,7 +333,7 @@ mod tests {
|
||||||
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
||||||
r#"{
|
r#"{
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"class": ["person"],
|
"class": ["person", "account"],
|
||||||
"name": ["testperson"],
|
"name": ["testperson"],
|
||||||
"description": ["testperson"],
|
"description": ["testperson"],
|
||||||
"displayname": ["testperson"]
|
"displayname": ["testperson"]
|
||||||
|
@ -369,7 +369,7 @@ mod tests {
|
||||||
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
||||||
r#"{
|
r#"{
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"class": ["person"],
|
"class": ["person", "account"],
|
||||||
"name": ["testperson"],
|
"name": ["testperson"],
|
||||||
"description": ["testperson"],
|
"description": ["testperson"],
|
||||||
"displayname": ["testperson"],
|
"displayname": ["testperson"],
|
||||||
|
@ -399,7 +399,7 @@ mod tests {
|
||||||
let mut e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
let mut e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
||||||
r#"{
|
r#"{
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"class": ["person"],
|
"class": ["person", "account"],
|
||||||
"name": ["testperson"],
|
"name": ["testperson"],
|
||||||
"description": ["testperson"],
|
"description": ["testperson"],
|
||||||
"displayname": ["testperson"],
|
"displayname": ["testperson"],
|
||||||
|
@ -432,7 +432,7 @@ mod tests {
|
||||||
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
||||||
r#"{
|
r#"{
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"class": ["person"],
|
"class": ["person", "account"],
|
||||||
"name": ["testperson"],
|
"name": ["testperson"],
|
||||||
"description": ["testperson"],
|
"description": ["testperson"],
|
||||||
"displayname": ["testperson"],
|
"displayname": ["testperson"],
|
||||||
|
@ -506,7 +506,7 @@ mod tests {
|
||||||
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
||||||
r#"{
|
r#"{
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"class": ["person"],
|
"class": ["account", "person"],
|
||||||
"name": ["testperson"],
|
"name": ["testperson"],
|
||||||
"description": ["testperson"],
|
"description": ["testperson"],
|
||||||
"displayname": ["testperson"],
|
"displayname": ["testperson"],
|
||||||
|
|
|
@ -104,7 +104,7 @@ impl Domain {
|
||||||
|
|
||||||
// Setup the minimum functional level if one is not set already.
|
// Setup the minimum functional level if one is not set already.
|
||||||
if !e.attribute_pres(Attribute::Version) {
|
if !e.attribute_pres(Attribute::Version) {
|
||||||
let n = Value::Uint32(DOMAIN_MIN_LEVEL);
|
let n = Value::Uint32(DOMAIN_LEVEL_0);
|
||||||
e.set_ava(Attribute::Version, once(n));
|
e.set_ava(Attribute::Version, once(n));
|
||||||
warn!("plugin_domain: Applying domain version transform");
|
warn!("plugin_domain: Applying domain version transform");
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -244,6 +244,7 @@ impl DynGroup {
|
||||||
pre_cand: &[Arc<Entry<EntrySealed, EntryCommitted>>],
|
pre_cand: &[Arc<Entry<EntrySealed, EntryCommitted>>],
|
||||||
cand: &[Entry<EntrySealed, EntryCommitted>],
|
cand: &[Entry<EntrySealed, EntryCommitted>],
|
||||||
_ident: &Identity,
|
_ident: &Identity,
|
||||||
|
force_cand_updates: bool,
|
||||||
) -> Result<Vec<Uuid>, OperationError> {
|
) -> Result<Vec<Uuid>, OperationError> {
|
||||||
let mut affected_uuids = Vec::with_capacity(cand.len());
|
let mut affected_uuids = Vec::with_capacity(cand.len());
|
||||||
|
|
||||||
|
@ -290,8 +291,7 @@ impl DynGroup {
|
||||||
|
|
||||||
// If we modified anything else, check if a dyngroup is affected by it's change
|
// If we modified anything else, check if a dyngroup is affected by it's change
|
||||||
// if it was a member.
|
// if it was a member.
|
||||||
|
trace!(?force_cand_updates, ?dyn_groups.insts);
|
||||||
trace!(?dyn_groups.insts);
|
|
||||||
|
|
||||||
for (dg_uuid, dg_filter) in dyn_groups.insts.iter() {
|
for (dg_uuid, dg_filter) in dyn_groups.insts.iter() {
|
||||||
let dg_filter_valid = dg_filter
|
let dg_filter_valid = dg_filter
|
||||||
|
@ -308,16 +308,26 @@ impl DynGroup {
|
||||||
let pre_t = pre.entry_match_no_index(&dg_filter_valid);
|
let pre_t = pre.entry_match_no_index(&dg_filter_valid);
|
||||||
let post_t = post.entry_match_no_index(&dg_filter_valid);
|
let post_t = post.entry_match_no_index(&dg_filter_valid);
|
||||||
|
|
||||||
if pre_t && !post_t {
|
trace!(?post_t, ?force_cand_updates, ?pre_t);
|
||||||
Some(Err(post.get_uuid()))
|
|
||||||
} else if !pre_t && post_t {
|
// There are some cases where rather than the optimisation to skip
|
||||||
|
// asserting membership, we need to always assert that membership. Generally
|
||||||
|
// this occurs in replication where if a candidate was conflicted it can
|
||||||
|
// trigger a membership delete, but we need to ensure it's still re-added.
|
||||||
|
if post_t && (force_cand_updates || !pre_t) {
|
||||||
|
// The entry was added
|
||||||
Some(Ok(post.get_uuid()))
|
Some(Ok(post.get_uuid()))
|
||||||
|
} else if pre_t && !post_t {
|
||||||
|
// The entry was deleted
|
||||||
|
Some(Err(post.get_uuid()))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
trace!(?matches);
|
||||||
|
|
||||||
if !matches.is_empty() {
|
if !matches.is_empty() {
|
||||||
let filt = filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(*dg_uuid)));
|
let filt = filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(*dg_uuid)));
|
||||||
let mut work_set = qs.internal_search_writeable(&filt)?;
|
let mut work_set = qs.internal_search_writeable(&filt)?;
|
||||||
|
@ -348,6 +358,8 @@ impl DynGroup {
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trace!(?affected_uuids);
|
||||||
|
|
||||||
Ok(affected_uuids)
|
Ok(affected_uuids)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -114,6 +114,7 @@ mod tests {
|
||||||
let uuid = Uuid::new_v4();
|
let uuid = Uuid::new_v4();
|
||||||
let e: Entry<EntryInit, EntryNew> = entry_init!(
|
let e: Entry<EntryInit, EntryNew> = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(
|
(
|
||||||
Attribute::Class,
|
Attribute::Class,
|
||||||
EntryClass::OAuth2ResourceServer.to_value()
|
EntryClass::OAuth2ResourceServer.to_value()
|
||||||
|
@ -127,10 +128,7 @@ mod tests {
|
||||||
Attribute::DisplayName,
|
Attribute::DisplayName,
|
||||||
Value::new_utf8s("test_resource_server")
|
Value::new_utf8s("test_resource_server")
|
||||||
),
|
),
|
||||||
(
|
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||||
Attribute::OAuth2RsName,
|
|
||||||
Value::new_iname("test_resource_server")
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
Attribute::OAuth2RsOrigin,
|
Attribute::OAuth2RsOrigin,
|
||||||
Value::new_url_s("https://demo.example.com").unwrap()
|
Value::new_url_s("https://demo.example.com").unwrap()
|
||||||
|
@ -168,6 +166,7 @@ mod tests {
|
||||||
|
|
||||||
let e: Entry<EntryInit, EntryNew> = entry_init!(
|
let e: Entry<EntryInit, EntryNew> = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(
|
(
|
||||||
Attribute::Class,
|
Attribute::Class,
|
||||||
EntryClass::OAuth2ResourceServer.to_value()
|
EntryClass::OAuth2ResourceServer.to_value()
|
||||||
|
@ -177,10 +176,7 @@ mod tests {
|
||||||
EntryClass::OAuth2ResourceServerBasic.to_value()
|
EntryClass::OAuth2ResourceServerBasic.to_value()
|
||||||
),
|
),
|
||||||
(Attribute::Uuid, Value::Uuid(uuid)),
|
(Attribute::Uuid, Value::Uuid(uuid)),
|
||||||
(
|
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||||
Attribute::OAuth2RsName,
|
|
||||||
Value::new_iname("test_resource_server")
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
Attribute::DisplayName,
|
Attribute::DisplayName,
|
||||||
Value::new_utf8s("test_resource_server")
|
Value::new_utf8s("test_resource_server")
|
||||||
|
|
|
@ -235,12 +235,26 @@ impl Plugin for MemberOf {
|
||||||
qs: &mut QueryServerWriteTransaction,
|
qs: &mut QueryServerWriteTransaction,
|
||||||
pre_cand: &[Arc<EntrySealedCommitted>],
|
pre_cand: &[Arc<EntrySealedCommitted>],
|
||||||
cand: &[EntrySealedCommitted],
|
cand: &[EntrySealedCommitted],
|
||||||
_conflict_uuids: &BTreeSet<Uuid>,
|
conflict_uuids: &BTreeSet<Uuid>,
|
||||||
) -> Result<(), OperationError> {
|
) -> Result<(), OperationError> {
|
||||||
|
// If a uuid was in a conflict state, it will be present in the cand/pre_cand set,
|
||||||
|
// but it *may not* trigger dyn groups as the conflict before and after may satisfy
|
||||||
|
// the filter as it exists.
|
||||||
|
//
|
||||||
|
// In these cases we need to force dynmembers to be reloaded if any conflict occurs
|
||||||
|
// to ensure that all our memberships are accurate.
|
||||||
|
let force_dyngroup_cand_update = !conflict_uuids.is_empty();
|
||||||
|
|
||||||
// IMPORTANT - we need this for now so that dyngroup doesn't error on us, since
|
// IMPORTANT - we need this for now so that dyngroup doesn't error on us, since
|
||||||
// repl is internal and dyngroup has a safety check to prevent external triggers.
|
// repl is internal and dyngroup has a safety check to prevent external triggers.
|
||||||
let ident_internal = Identity::from_internal();
|
let ident_internal = Identity::from_internal();
|
||||||
Self::post_modify_inner(qs, pre_cand, cand, &ident_internal)
|
Self::post_modify_inner(
|
||||||
|
qs,
|
||||||
|
pre_cand,
|
||||||
|
cand,
|
||||||
|
&ident_internal,
|
||||||
|
force_dyngroup_cand_update,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "debug", name = "memberof_post_modify", skip_all)]
|
#[instrument(level = "debug", name = "memberof_post_modify", skip_all)]
|
||||||
|
@ -250,7 +264,7 @@ impl Plugin for MemberOf {
|
||||||
cand: &[Entry<EntrySealed, EntryCommitted>],
|
cand: &[Entry<EntrySealed, EntryCommitted>],
|
||||||
me: &ModifyEvent,
|
me: &ModifyEvent,
|
||||||
) -> Result<(), OperationError> {
|
) -> Result<(), OperationError> {
|
||||||
Self::post_modify_inner(qs, pre_cand, cand, &me.ident)
|
Self::post_modify_inner(qs, pre_cand, cand, &me.ident, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "debug", name = "memberof_post_batch_modify", skip_all)]
|
#[instrument(level = "debug", name = "memberof_post_batch_modify", skip_all)]
|
||||||
|
@ -260,7 +274,29 @@ impl Plugin for MemberOf {
|
||||||
cand: &[Entry<EntrySealed, EntryCommitted>],
|
cand: &[Entry<EntrySealed, EntryCommitted>],
|
||||||
me: &BatchModifyEvent,
|
me: &BatchModifyEvent,
|
||||||
) -> Result<(), OperationError> {
|
) -> Result<(), OperationError> {
|
||||||
Self::post_modify_inner(qs, pre_cand, cand, &me.ident)
|
Self::post_modify_inner(qs, pre_cand, cand, &me.ident, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", name = "memberof_pre_delete", skip_all)]
|
||||||
|
fn pre_delete(
|
||||||
|
_qs: &mut QueryServerWriteTransaction,
|
||||||
|
cand: &mut Vec<EntryInvalidCommitted>,
|
||||||
|
_de: &DeleteEvent,
|
||||||
|
) -> Result<(), OperationError> {
|
||||||
|
// Ensure that when an entry is deleted, that we remove its memberof values,
|
||||||
|
// and convert direct memberof to recycled direct memberof.
|
||||||
|
|
||||||
|
for entry in cand.iter_mut() {
|
||||||
|
if let Some(direct_mo_vs) = entry.pop_ava(Attribute::DirectMemberOf) {
|
||||||
|
entry.set_ava_set(Attribute::RecycledDirectMemberOf, direct_mo_vs);
|
||||||
|
} else {
|
||||||
|
// Ensure it's empty
|
||||||
|
entry.purge_ava(Attribute::RecycledDirectMemberOf);
|
||||||
|
}
|
||||||
|
entry.purge_ava(Attribute::MemberOf);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "debug", name = "memberof_post_delete", skip_all)]
|
#[instrument(level = "debug", name = "memberof_post_delete", skip_all)]
|
||||||
|
@ -368,11 +404,14 @@ impl Plugin for MemberOf {
|
||||||
(None, None) => {
|
(None, None) => {
|
||||||
// Ok
|
// Ok
|
||||||
}
|
}
|
||||||
_ => {
|
(entry_dmo, d_groups) => {
|
||||||
admin_error!(
|
admin_error!(
|
||||||
"MemberOfInvalid directmemberof set and DMO search set differ in size: {}",
|
"MemberOfInvalid directmemberof set and DMO search set differ in size: {}",
|
||||||
e.get_uuid()
|
e.get_uuid()
|
||||||
);
|
);
|
||||||
|
trace!(?e);
|
||||||
|
trace!(?entry_dmo);
|
||||||
|
trace!(?d_groups);
|
||||||
r.push(Err(ConsistencyError::MemberOfInvalid(e.get_id())));
|
r.push(Err(ConsistencyError::MemberOfInvalid(e.get_id())));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -429,8 +468,15 @@ impl MemberOf {
|
||||||
pre_cand: &[Arc<EntrySealedCommitted>],
|
pre_cand: &[Arc<EntrySealedCommitted>],
|
||||||
cand: &[EntrySealedCommitted],
|
cand: &[EntrySealedCommitted],
|
||||||
ident: &Identity,
|
ident: &Identity,
|
||||||
|
force_dyngroup_cand_update: bool,
|
||||||
) -> Result<(), OperationError> {
|
) -> Result<(), OperationError> {
|
||||||
let dyngroup_change = super::dyngroup::DynGroup::post_modify(qs, pre_cand, cand, ident)?;
|
let dyngroup_change = super::dyngroup::DynGroup::post_modify(
|
||||||
|
qs,
|
||||||
|
pre_cand,
|
||||||
|
cand,
|
||||||
|
ident,
|
||||||
|
force_dyngroup_cand_update,
|
||||||
|
)?;
|
||||||
|
|
||||||
// TODO: Limit this to when it's a class, member, mo, dmo change instead.
|
// TODO: Limit this to when it's a class, member, mo, dmo change instead.
|
||||||
let group_affect = cand
|
let group_affect = cand
|
||||||
|
|
|
@ -338,7 +338,8 @@ impl Plugins {
|
||||||
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
|
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
|
||||||
de: &DeleteEvent,
|
de: &DeleteEvent,
|
||||||
) -> Result<(), OperationError> {
|
) -> Result<(), OperationError> {
|
||||||
protected::Protected::pre_delete(qs, cand, de)
|
protected::Protected::pre_delete(qs, cand, de)?;
|
||||||
|
memberof::MemberOf::pre_delete(qs, cand, de)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "debug", name = "plugins::run_post_delete", skip_all)]
|
#[instrument(level = "debug", name = "plugins::run_post_delete", skip_all)]
|
||||||
|
|
|
@ -397,6 +397,7 @@ mod tests {
|
||||||
Attribute::PrivateCookieKey.to_value()
|
Attribute::PrivateCookieKey.to_value()
|
||||||
),
|
),
|
||||||
(Attribute::AcpCreateClass, EntryClass::Object.to_value()),
|
(Attribute::AcpCreateClass, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::AcpCreateClass, EntryClass::Account.to_value()),
|
||||||
(Attribute::AcpCreateClass, EntryClass::Person.to_value()),
|
(Attribute::AcpCreateClass, EntryClass::Person.to_value()),
|
||||||
(Attribute::AcpCreateClass, EntryClass::System.to_value()),
|
(Attribute::AcpCreateClass, EntryClass::System.to_value()),
|
||||||
(Attribute::AcpCreateClass, EntryClass::DomainInfo.to_value()),
|
(Attribute::AcpCreateClass, EntryClass::DomainInfo.to_value()),
|
||||||
|
@ -438,7 +439,7 @@ mod tests {
|
||||||
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
||||||
r#"{
|
r#"{
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"class": ["person", "system"],
|
"class": ["account", "person", "system"],
|
||||||
"name": ["testperson"],
|
"name": ["testperson"],
|
||||||
"description": ["testperson"],
|
"description": ["testperson"],
|
||||||
"displayname": ["testperson"]
|
"displayname": ["testperson"]
|
||||||
|
@ -464,7 +465,7 @@ mod tests {
|
||||||
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
||||||
r#"{
|
r#"{
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"class": ["person", "system"],
|
"class": ["account", "person", "system"],
|
||||||
"name": ["testperson"],
|
"name": ["testperson"],
|
||||||
"description": ["testperson"],
|
"description": ["testperson"],
|
||||||
"displayname": ["testperson"]
|
"displayname": ["testperson"]
|
||||||
|
@ -526,7 +527,7 @@ mod tests {
|
||||||
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
let e: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(
|
||||||
r#"{
|
r#"{
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"class": ["person", "system"],
|
"class": ["account", "person", "system"],
|
||||||
"name": ["testperson"],
|
"name": ["testperson"],
|
||||||
"description": ["testperson"],
|
"description": ["testperson"],
|
||||||
"displayname": ["testperson"]
|
"displayname": ["testperson"]
|
||||||
|
|
|
@ -990,15 +990,13 @@ mod tests {
|
||||||
// scope map is also appropriately affected.
|
// scope map is also appropriately affected.
|
||||||
let ea: Entry<EntryInit, EntryNew> = entry_init!(
|
let ea: Entry<EntryInit, EntryNew> = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(
|
(
|
||||||
Attribute::Class,
|
Attribute::Class,
|
||||||
EntryClass::OAuth2ResourceServer.to_value()
|
EntryClass::OAuth2ResourceServer.to_value()
|
||||||
),
|
),
|
||||||
// (Attribute::Class, EntryClass::OAuth2ResourceServerBasic.into()),
|
// (Attribute::Class, EntryClass::OAuth2ResourceServerBasic.into()),
|
||||||
(
|
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||||
Attribute::OAuth2RsName,
|
|
||||||
Value::new_iname("test_resource_server")
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
Attribute::DisplayName,
|
Attribute::DisplayName,
|
||||||
Value::new_utf8s("test_resource_server")
|
Value::new_utf8s("test_resource_server")
|
||||||
|
@ -1037,7 +1035,7 @@ mod tests {
|
||||||
|qs: &mut QueryServerWriteTransaction| {
|
|qs: &mut QueryServerWriteTransaction| {
|
||||||
let cands = qs
|
let cands = qs
|
||||||
.internal_search(filter!(f_eq(
|
.internal_search(filter!(f_eq(
|
||||||
Attribute::OAuth2RsName,
|
Attribute::Name,
|
||||||
PartialValue::new_iname("test_resource_server")
|
PartialValue::new_iname("test_resource_server")
|
||||||
)))
|
)))
|
||||||
.expect("Internal search failure");
|
.expect("Internal search failure");
|
||||||
|
@ -1080,15 +1078,13 @@ mod tests {
|
||||||
|
|
||||||
let e2 = entry_init!(
|
let e2 = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(
|
(
|
||||||
Attribute::Class,
|
Attribute::Class,
|
||||||
EntryClass::OAuth2ResourceServer.to_value()
|
EntryClass::OAuth2ResourceServer.to_value()
|
||||||
),
|
),
|
||||||
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
||||||
(
|
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||||
Attribute::OAuth2RsName,
|
|
||||||
Value::new_iname("test_resource_server")
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
Attribute::DisplayName,
|
Attribute::DisplayName,
|
||||||
Value::new_utf8s("test_resource_server")
|
Value::new_utf8s("test_resource_server")
|
||||||
|
@ -1129,7 +1125,7 @@ mod tests {
|
||||||
Value::Oauth2Session(
|
Value::Oauth2Session(
|
||||||
session_id,
|
session_id,
|
||||||
Oauth2Session {
|
Oauth2Session {
|
||||||
parent: parent_id,
|
parent: Some(parent_id),
|
||||||
// Note we set the exp to None so we are not removing based on exp
|
// Note we set the exp to None so we are not removing based on exp
|
||||||
state: SessionState::NeverExpires,
|
state: SessionState::NeverExpires,
|
||||||
issued_at,
|
issued_at,
|
||||||
|
@ -1309,6 +1305,7 @@ mod tests {
|
||||||
fn test_delete_remove_reference_oauth2_claim_map() {
|
fn test_delete_remove_reference_oauth2_claim_map() {
|
||||||
let ea: Entry<EntryInit, EntryNew> = entry_init!(
|
let ea: Entry<EntryInit, EntryNew> = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(
|
(
|
||||||
Attribute::Class,
|
Attribute::Class,
|
||||||
EntryClass::OAuth2ResourceServer.to_value()
|
EntryClass::OAuth2ResourceServer.to_value()
|
||||||
|
@ -1317,10 +1314,7 @@ mod tests {
|
||||||
Attribute::Class,
|
Attribute::Class,
|
||||||
EntryClass::OAuth2ResourceServerPublic.to_value()
|
EntryClass::OAuth2ResourceServerPublic.to_value()
|
||||||
),
|
),
|
||||||
(
|
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||||
Attribute::OAuth2RsName,
|
|
||||||
Value::new_iname("test_resource_server")
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
Attribute::DisplayName,
|
Attribute::DisplayName,
|
||||||
Value::new_utf8s("test_resource_server")
|
Value::new_utf8s("test_resource_server")
|
||||||
|
@ -1366,7 +1360,7 @@ mod tests {
|
||||||
|qs: &mut QueryServerWriteTransaction| {
|
|qs: &mut QueryServerWriteTransaction| {
|
||||||
let cands = qs
|
let cands = qs
|
||||||
.internal_search(filter!(f_eq(
|
.internal_search(filter!(f_eq(
|
||||||
Attribute::OAuth2RsName,
|
Attribute::Name,
|
||||||
PartialValue::new_iname("test_resource_server")
|
PartialValue::new_iname("test_resource_server")
|
||||||
)))
|
)))
|
||||||
.expect("Internal search failure");
|
.expect("Internal search failure");
|
||||||
|
|
|
@ -131,13 +131,20 @@ impl SessionConsistency {
|
||||||
_ => {
|
_ => {
|
||||||
// Okay, now check the issued / grace time for parent enforcement.
|
// Okay, now check the issued / grace time for parent enforcement.
|
||||||
if sessions.map(|session_map| {
|
if sessions.map(|session_map| {
|
||||||
if let Some(parent_session) = session_map.get(&session.parent) {
|
if let Some(parent_session_id) = session.parent.as_ref() {
|
||||||
|
// A parent session id exists - validate it exists in the account.
|
||||||
|
if let Some(parent_session) = session_map.get(parent_session_id) {
|
||||||
// Only match non-revoked sessions
|
// Only match non-revoked sessions
|
||||||
!matches!(parent_session.state, SessionState::RevokedAt(_))
|
!matches!(parent_session.state, SessionState::RevokedAt(_))
|
||||||
} else {
|
} else {
|
||||||
// not found
|
// not found
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// The session specifically has no parent session and so is
|
||||||
|
// not bounded by it's presence.
|
||||||
|
true
|
||||||
|
}
|
||||||
}).unwrap_or(false) {
|
}).unwrap_or(false) {
|
||||||
// The parent exists and is still valid, go ahead
|
// The parent exists and is still valid, go ahead
|
||||||
debug!("Parent session remains valid.");
|
debug!("Parent session remains valid.");
|
||||||
|
@ -145,7 +152,7 @@ impl SessionConsistency {
|
||||||
} else {
|
} else {
|
||||||
// Can't find the parent. Are we within grace window
|
// Can't find the parent. Are we within grace window
|
||||||
if session.issued_at + GRACE_WINDOW <= curtime_odt {
|
if session.issued_at + GRACE_WINDOW <= curtime_odt {
|
||||||
info!(%o2_session_id, parent_id = %session.parent, "Removing orphaned oauth2 session");
|
info!(%o2_session_id, parent_id = ?session.parent, "Removing orphaned oauth2 session");
|
||||||
Some(PartialValue::Refer(*o2_session_id))
|
Some(PartialValue::Refer(*o2_session_id))
|
||||||
} else {
|
} else {
|
||||||
// Grace window is still in effect
|
// Grace window is still in effect
|
||||||
|
@ -330,6 +337,7 @@ mod tests {
|
||||||
|
|
||||||
let e2 = entry_init!(
|
let e2 = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(
|
(
|
||||||
Attribute::Class,
|
Attribute::Class,
|
||||||
EntryClass::OAuth2ResourceServer.to_value()
|
EntryClass::OAuth2ResourceServer.to_value()
|
||||||
|
@ -339,10 +347,7 @@ mod tests {
|
||||||
EntryClass::OAuth2ResourceServerBasic.to_value()
|
EntryClass::OAuth2ResourceServerBasic.to_value()
|
||||||
),
|
),
|
||||||
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
||||||
(
|
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||||
Attribute::OAuth2RsName,
|
|
||||||
Value::new_iname("test_resource_server")
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
Attribute::DisplayName,
|
Attribute::DisplayName,
|
||||||
Value::new_utf8s("test_resource_server")
|
Value::new_utf8s("test_resource_server")
|
||||||
|
@ -381,7 +386,7 @@ mod tests {
|
||||||
Value::Oauth2Session(
|
Value::Oauth2Session(
|
||||||
session_id,
|
session_id,
|
||||||
Oauth2Session {
|
Oauth2Session {
|
||||||
parent: parent_id,
|
parent: Some(parent_id),
|
||||||
// Set to the exp window.
|
// Set to the exp window.
|
||||||
state,
|
state,
|
||||||
issued_at,
|
issued_at,
|
||||||
|
@ -505,6 +510,7 @@ mod tests {
|
||||||
|
|
||||||
let e2 = entry_init!(
|
let e2 = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(
|
(
|
||||||
Attribute::Class,
|
Attribute::Class,
|
||||||
EntryClass::OAuth2ResourceServer.to_value()
|
EntryClass::OAuth2ResourceServer.to_value()
|
||||||
|
@ -514,10 +520,7 @@ mod tests {
|
||||||
EntryClass::OAuth2ResourceServerBasic.to_value()
|
EntryClass::OAuth2ResourceServerBasic.to_value()
|
||||||
),
|
),
|
||||||
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
||||||
(
|
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||||
Attribute::OAuth2RsName,
|
|
||||||
Value::new_iname("test_resource_server")
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
Attribute::DisplayName,
|
Attribute::DisplayName,
|
||||||
Value::new_utf8s("test_resource_server")
|
Value::new_utf8s("test_resource_server")
|
||||||
|
@ -555,7 +558,7 @@ mod tests {
|
||||||
Value::Oauth2Session(
|
Value::Oauth2Session(
|
||||||
session_id,
|
session_id,
|
||||||
Oauth2Session {
|
Oauth2Session {
|
||||||
parent: parent_id,
|
parent: Some(parent_id),
|
||||||
// Note we set the exp to None so we are not removing based on exp
|
// Note we set the exp to None so we are not removing based on exp
|
||||||
state: SessionState::NeverExpires,
|
state: SessionState::NeverExpires,
|
||||||
issued_at,
|
issued_at,
|
||||||
|
@ -673,6 +676,7 @@ mod tests {
|
||||||
|
|
||||||
let e2 = entry_init!(
|
let e2 = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(
|
(
|
||||||
Attribute::Class,
|
Attribute::Class,
|
||||||
EntryClass::OAuth2ResourceServer.to_value()
|
EntryClass::OAuth2ResourceServer.to_value()
|
||||||
|
@ -682,10 +686,7 @@ mod tests {
|
||||||
EntryClass::OAuth2ResourceServerBasic.to_value()
|
EntryClass::OAuth2ResourceServerBasic.to_value()
|
||||||
),
|
),
|
||||||
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
||||||
(
|
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||||
Attribute::OAuth2RsName,
|
|
||||||
Value::new_iname("test_resource_server")
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
Attribute::DisplayName,
|
Attribute::DisplayName,
|
||||||
Value::new_utf8s("test_resource_server")
|
Value::new_utf8s("test_resource_server")
|
||||||
|
@ -716,7 +717,7 @@ mod tests {
|
||||||
let session = Value::Oauth2Session(
|
let session = Value::Oauth2Session(
|
||||||
session_id,
|
session_id,
|
||||||
Oauth2Session {
|
Oauth2Session {
|
||||||
parent,
|
parent: Some(parent),
|
||||||
// Note we set the exp to None so we are asserting the removal is due to the lack
|
// Note we set the exp to None so we are asserting the removal is due to the lack
|
||||||
// of the parent session.
|
// of the parent session.
|
||||||
state: SessionState::NeverExpires,
|
state: SessionState::NeverExpires,
|
||||||
|
|
|
@ -136,6 +136,7 @@ mod tests {
|
||||||
assert!(server_txn
|
assert!(server_txn
|
||||||
.internal_create(vec![entry_init!(
|
.internal_create(vec![entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("tobias")),
|
(Attribute::Name, Value::new_iname("tobias")),
|
||||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||||
|
@ -154,6 +155,7 @@ mod tests {
|
||||||
assert!(server_txn
|
assert!(server_txn
|
||||||
.internal_create(vec![entry_init!(
|
.internal_create(vec![entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("newname")),
|
(Attribute::Name, Value::new_iname("newname")),
|
||||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||||
|
@ -181,6 +183,7 @@ mod tests {
|
||||||
assert!(server_txn
|
assert!(server_txn
|
||||||
.internal_create(vec![entry_init!(
|
.internal_create(vec![entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("newname")),
|
(Attribute::Name, Value::new_iname("newname")),
|
||||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||||
|
|
|
@ -184,10 +184,18 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
//
|
//
|
||||||
let (cand, pre_cand): (Vec<_>, Vec<_>) = all_updates_valid
|
let (cand, pre_cand): (Vec<_>, Vec<_>) = all_updates_valid
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
// We previously excluded this to avoid doing unnecesary work on entries that
|
||||||
|
// were moving to a conflict state, and the survivor was staying "as is" on this
|
||||||
|
// node. However, this gets messy with dyngroups and memberof, where on a conflict
|
||||||
|
// the memberships are deleted across the replication boundary. In these cases
|
||||||
|
// we need dyngroups to see the valid entries, even if they are "identical to before"
|
||||||
|
// to re-assert all their memberships are valid.
|
||||||
|
/*
|
||||||
.filter(|(cand, _)| {
|
.filter(|(cand, _)| {
|
||||||
// Exclude anything that is conflicted as a result of the conflict plugins.
|
// Exclude anything that is conflicted as a result of the conflict plugins.
|
||||||
!conflict_uuids.contains(&cand.get_uuid())
|
!conflict_uuids.contains(&cand.get_uuid())
|
||||||
})
|
})
|
||||||
|
*/
|
||||||
.unzip();
|
.unzip();
|
||||||
|
|
||||||
// We don't need to process conflict_creates here, since they are all conflicting
|
// We don't need to process conflict_creates here, since they are all conflicting
|
||||||
|
|
|
@ -215,9 +215,9 @@ impl EntryChangeState {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn get_attr_cid(&self, attr: &Attribute) -> Option<Cid> {
|
pub(crate) fn get_attr_cid(&self, attr: &Attribute) -> Option<&Cid> {
|
||||||
match &self.st {
|
match &self.st {
|
||||||
State::Live { at: _, changes } => changes.get(attr.as_ref()).map(|cid| cid.clone()),
|
State::Live { at: _, changes } => changes.get(attr.as_ref()),
|
||||||
State::Tombstone { at: _ } => None,
|
State::Tombstone { at: _ } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ use crate::schema::{SchemaReadTransaction, SchemaTransaction};
|
||||||
use crate::valueset;
|
use crate::valueset;
|
||||||
use base64urlsafedata::Base64UrlSafeData;
|
use base64urlsafedata::Base64UrlSafeData;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_with::skip_serializing_none;
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
use webauthn_rs::prelude::{
|
use webauthn_rs::prelude::{
|
||||||
|
@ -261,10 +262,11 @@ pub struct ReplOauthClaimMapV1 {
|
||||||
pub values: BTreeMap<Uuid, BTreeSet<String>>,
|
pub values: BTreeMap<Uuid, BTreeSet<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||||
pub struct ReplOauth2SessionV1 {
|
pub struct ReplOauth2SessionV1 {
|
||||||
pub refer: Uuid,
|
pub refer: Uuid,
|
||||||
pub parent: Uuid,
|
pub parent: Option<Uuid>,
|
||||||
pub state: ReplSessionStateV1,
|
pub state: ReplSessionStateV1,
|
||||||
// pub expiry: Option<String>,
|
// pub expiry: Option<String>,
|
||||||
pub issued_at: String,
|
pub issued_at: String,
|
||||||
|
|
|
@ -148,9 +148,13 @@ impl<'a> QueryServerReadTransaction<'a> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!(?ranges, "these ranges will be supplied");
|
debug!("these ranges will be supplied");
|
||||||
|
debug!(supply_ranges = ?ranges);
|
||||||
|
debug!(consumer_ranges = ?ctx_ranges);
|
||||||
|
debug!(supplier_ranges = ?our_ranges);
|
||||||
|
|
||||||
if ranges.is_empty() {
|
if ranges.is_empty() {
|
||||||
|
debug!("No Changes Available");
|
||||||
return Ok(ReplIncrementalContext::NoChangesAvailable);
|
return Ok(ReplIncrementalContext::NoChangesAvailable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -250,6 +250,7 @@ async fn test_repl_increment_basic_entry_add(server_a: &QueryServer, server_b: &
|
||||||
assert!(server_b_txn
|
assert!(server_b_txn
|
||||||
.internal_create(vec![entry_init!(
|
.internal_create(vec![entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||||
|
@ -353,6 +354,7 @@ async fn test_repl_increment_basic_entry_recycle(server_a: &QueryServer, server_
|
||||||
assert!(server_b_txn
|
assert!(server_b_txn
|
||||||
.internal_create(vec![entry_init!(
|
.internal_create(vec![entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||||
|
@ -411,6 +413,7 @@ async fn test_repl_increment_basic_entry_tombstone(server_a: &QueryServer, serve
|
||||||
assert!(server_b_txn
|
assert!(server_b_txn
|
||||||
.internal_create(vec![entry_init!(
|
.internal_create(vec![entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||||
|
@ -481,6 +484,7 @@ async fn test_repl_increment_consumer_lagging_tombstone(
|
||||||
assert!(server_b_txn
|
assert!(server_b_txn
|
||||||
.internal_create(vec![entry_init!(
|
.internal_create(vec![entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||||
|
@ -584,6 +588,7 @@ async fn test_repl_increment_basic_bidirectional_write(
|
||||||
assert!(server_b_txn
|
assert!(server_b_txn
|
||||||
.internal_create(vec![entry_init!(
|
.internal_create(vec![entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||||
|
@ -664,6 +669,7 @@ async fn test_repl_increment_basic_deleted_attr(server_a: &QueryServer, server_b
|
||||||
assert!(server_a_txn
|
assert!(server_a_txn
|
||||||
.internal_create(vec![entry_init!(
|
.internal_create(vec![entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||||
|
@ -731,6 +737,7 @@ async fn test_repl_increment_simultaneous_bidirectional_write(
|
||||||
assert!(server_b_txn
|
assert!(server_b_txn
|
||||||
.internal_create(vec![entry_init!(
|
.internal_create(vec![entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||||
|
@ -842,6 +849,7 @@ async fn test_repl_increment_basic_bidirectional_lifecycle(
|
||||||
assert!(server_b_txn
|
assert!(server_b_txn
|
||||||
.internal_create(vec![entry_init!(
|
.internal_create(vec![entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||||
|
@ -983,6 +991,7 @@ async fn test_repl_increment_basic_bidirectional_recycle(
|
||||||
assert!(server_b_txn
|
assert!(server_b_txn
|
||||||
.internal_create(vec![entry_init!(
|
.internal_create(vec![entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||||
|
@ -1109,6 +1118,7 @@ async fn test_repl_increment_basic_bidirectional_tombstone(
|
||||||
assert!(server_b_txn
|
assert!(server_b_txn
|
||||||
.internal_create(vec![entry_init!(
|
.internal_create(vec![entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||||
|
@ -1213,6 +1223,7 @@ async fn test_repl_increment_creation_uuid_conflict(
|
||||||
let t_uuid = Uuid::new_v4();
|
let t_uuid = Uuid::new_v4();
|
||||||
let e_init = entry_init!(
|
let e_init = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||||
|
@ -1244,6 +1255,19 @@ async fn test_repl_increment_creation_uuid_conflict(
|
||||||
.internal_search_all_uuid(t_uuid)
|
.internal_search_all_uuid(t_uuid)
|
||||||
.expect("Unable to access entry.");
|
.expect("Unable to access entry.");
|
||||||
|
|
||||||
|
let e1_acc = server_a_txn
|
||||||
|
.internal_search_all_uuid(UUID_IDM_ALL_ACCOUNTS)
|
||||||
|
.expect("Unable to access new entry.");
|
||||||
|
let e2_acc = server_b_txn
|
||||||
|
.internal_search_all_uuid(UUID_IDM_ALL_ACCOUNTS)
|
||||||
|
.expect("Unable to access entry.");
|
||||||
|
|
||||||
|
trace!("TESTMARKER 0");
|
||||||
|
trace!(?e1);
|
||||||
|
trace!(?e2);
|
||||||
|
trace!(?e1_acc);
|
||||||
|
trace!(?e2_acc);
|
||||||
|
|
||||||
trace!("{:?}", e1.get_last_changed());
|
trace!("{:?}", e1.get_last_changed());
|
||||||
trace!("{:?}", e2.get_last_changed());
|
trace!("{:?}", e2.get_last_changed());
|
||||||
// e2 from b will be smaller as it's the older entry.
|
// e2 from b will be smaller as it's the older entry.
|
||||||
|
@ -1277,6 +1301,19 @@ async fn test_repl_increment_creation_uuid_conflict(
|
||||||
.internal_search_all_uuid(t_uuid)
|
.internal_search_all_uuid(t_uuid)
|
||||||
.expect("Unable to access entry.");
|
.expect("Unable to access entry.");
|
||||||
|
|
||||||
|
let e1_acc = server_a_txn
|
||||||
|
.internal_search_all_uuid(UUID_IDM_ALL_ACCOUNTS)
|
||||||
|
.expect("Unable to access new entry.");
|
||||||
|
let e2_acc = server_b_txn
|
||||||
|
.internal_search_all_uuid(UUID_IDM_ALL_ACCOUNTS)
|
||||||
|
.expect("Unable to access entry.");
|
||||||
|
|
||||||
|
trace!("TESTMARKER 1");
|
||||||
|
trace!(?e1);
|
||||||
|
trace!(?e2);
|
||||||
|
trace!(?e1_acc);
|
||||||
|
trace!(?e2_acc);
|
||||||
|
|
||||||
assert!(e1.get_last_changed() == e2.get_last_changed());
|
assert!(e1.get_last_changed() == e2.get_last_changed());
|
||||||
|
|
||||||
let cnf_a = server_a_txn
|
let cnf_a = server_a_txn
|
||||||
|
@ -1292,6 +1329,10 @@ async fn test_repl_increment_creation_uuid_conflict(
|
||||||
.expect("Unable to conflict entries.");
|
.expect("Unable to conflict entries.");
|
||||||
assert!(cnf_b.is_empty());
|
assert!(cnf_b.is_empty());
|
||||||
|
|
||||||
|
trace!("TESTMARKER 2");
|
||||||
|
trace!(?cnf_a);
|
||||||
|
trace!(?cnf_b);
|
||||||
|
|
||||||
server_a_txn.commit().expect("Failed to commit");
|
server_a_txn.commit().expect("Failed to commit");
|
||||||
drop(server_b_txn);
|
drop(server_b_txn);
|
||||||
|
|
||||||
|
@ -1319,8 +1360,32 @@ async fn test_repl_increment_creation_uuid_conflict(
|
||||||
.pop()
|
.pop()
|
||||||
.expect("No conflict entries present");
|
.expect("No conflict entries present");
|
||||||
|
|
||||||
|
trace!("TESTMARKER 3");
|
||||||
|
trace!(?cnf_a);
|
||||||
|
trace!(?cnf_b);
|
||||||
|
|
||||||
assert!(cnf_a.get_last_changed() == cnf_b.get_last_changed());
|
assert!(cnf_a.get_last_changed() == cnf_b.get_last_changed());
|
||||||
|
|
||||||
|
let e1 = server_a_txn
|
||||||
|
.internal_search_all_uuid(t_uuid)
|
||||||
|
.expect("Unable to access new entry.");
|
||||||
|
let e2 = server_b_txn
|
||||||
|
.internal_search_all_uuid(t_uuid)
|
||||||
|
.expect("Unable to access entry.");
|
||||||
|
|
||||||
|
let e1_acc = server_a_txn
|
||||||
|
.internal_search_all_uuid(UUID_IDM_ALL_ACCOUNTS)
|
||||||
|
.expect("Unable to access new entry.");
|
||||||
|
let e2_acc = server_b_txn
|
||||||
|
.internal_search_all_uuid(UUID_IDM_ALL_ACCOUNTS)
|
||||||
|
.expect("Unable to access entry.");
|
||||||
|
|
||||||
|
trace!("TESTMARKER 4");
|
||||||
|
trace!(?e1);
|
||||||
|
trace!(?e2);
|
||||||
|
trace!(?e1_acc);
|
||||||
|
trace!(?e2_acc);
|
||||||
|
|
||||||
server_b_txn.commit().expect("Failed to commit");
|
server_b_txn.commit().expect("Failed to commit");
|
||||||
drop(server_a_txn);
|
drop(server_a_txn);
|
||||||
}
|
}
|
||||||
|
@ -1344,6 +1409,7 @@ async fn test_repl_increment_create_tombstone_uuid_conflict(
|
||||||
let t_uuid = Uuid::new_v4();
|
let t_uuid = Uuid::new_v4();
|
||||||
let e_init = entry_init!(
|
let e_init = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||||
|
@ -1437,6 +1503,7 @@ async fn test_repl_increment_create_tombstone_conflict(
|
||||||
let t_uuid = Uuid::new_v4();
|
let t_uuid = Uuid::new_v4();
|
||||||
let e_init = entry_init!(
|
let e_init = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||||
|
@ -1533,6 +1600,7 @@ async fn test_repl_increment_schema_conflict(server_a: &QueryServer, server_b: &
|
||||||
assert!(server_b_txn
|
assert!(server_b_txn
|
||||||
.internal_create(vec![entry_init!(
|
.internal_create(vec![entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||||
|
@ -1570,8 +1638,10 @@ async fn test_repl_increment_schema_conflict(server_a: &QueryServer, server_b: &
|
||||||
let mut server_b_txn = server_b.write(ct).await;
|
let mut server_b_txn = server_b.write(ct).await;
|
||||||
let modlist = ModifyList::new_list(vec![
|
let modlist = ModifyList::new_list(vec![
|
||||||
Modify::Removed(Attribute::Class.into(), EntryClass::Person.into()),
|
Modify::Removed(Attribute::Class.into(), EntryClass::Person.into()),
|
||||||
|
Modify::Removed(Attribute::Class.into(), EntryClass::Account.into()),
|
||||||
Modify::Present(Attribute::Class.into(), EntryClass::Group.into()),
|
Modify::Present(Attribute::Class.into(), EntryClass::Group.into()),
|
||||||
Modify::Purged(Attribute::IdVerificationEcKey.into()),
|
Modify::Purged(Attribute::IdVerificationEcKey.into()),
|
||||||
|
Modify::Purged(Attribute::NameHistory.into()),
|
||||||
Modify::Purged(Attribute::DisplayName.into()),
|
Modify::Purged(Attribute::DisplayName.into()),
|
||||||
]);
|
]);
|
||||||
assert!(server_b_txn.internal_modify_uuid(t_uuid, &modlist).is_ok());
|
assert!(server_b_txn.internal_modify_uuid(t_uuid, &modlist).is_ok());
|
||||||
|
@ -1650,6 +1720,7 @@ async fn test_repl_increment_consumer_lagging_attributes(
|
||||||
assert!(server_b_txn
|
assert!(server_b_txn
|
||||||
.internal_create(vec![entry_init!(
|
.internal_create(vec![entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||||
|
@ -1775,6 +1846,7 @@ async fn test_repl_increment_consumer_ruv_trim_past_valid(
|
||||||
assert!(server_b_txn
|
assert!(server_b_txn
|
||||||
.internal_create(vec![entry_init!(
|
.internal_create(vec![entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||||
|
@ -1905,6 +1977,7 @@ async fn test_repl_increment_consumer_ruv_trim_idle_servers(
|
||||||
assert!(server_b_txn
|
assert!(server_b_txn
|
||||||
.internal_create(vec![entry_init!(
|
.internal_create(vec![entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||||
|
|
|
@ -1433,6 +1433,25 @@ impl<'a> SchemaWriteTransaction<'a> {
|
||||||
syntax: SyntaxType::ReferenceUuid,
|
syntax: SyntaxType::ReferenceUuid,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
self.attributes.insert(
|
||||||
|
Attribute::RecycledDirectMemberOf.into(),
|
||||||
|
SchemaAttribute {
|
||||||
|
name: Attribute::RecycledDirectMemberOf.into(),
|
||||||
|
uuid: UUID_SCHEMA_ATTR_RECYCLEDDIRECTMEMBEROF,
|
||||||
|
description: String::from("recycled reverse direct group membership of the object to assist in revive operations."),
|
||||||
|
multivalue: true,
|
||||||
|
unique: false,
|
||||||
|
phantom: false,
|
||||||
|
sync_allowed: false,
|
||||||
|
// Unlike DMO this must be replicated so that on a recycle event, these groups
|
||||||
|
// "at delete" are replicated to partners. This avoids us having to replicate
|
||||||
|
// DMO which is very costly, while still retaining our ability to revive entries
|
||||||
|
// and their group memberships as a best effort.
|
||||||
|
replicated: true,
|
||||||
|
index: vec![],
|
||||||
|
syntax: SyntaxType::ReferenceUuid,
|
||||||
|
},
|
||||||
|
);
|
||||||
self.attributes.insert(
|
self.attributes.insert(
|
||||||
Attribute::Member.into(),
|
Attribute::Member.into(),
|
||||||
SchemaAttribute {
|
SchemaAttribute {
|
||||||
|
@ -1974,6 +1993,7 @@ impl<'a> SchemaWriteTransaction<'a> {
|
||||||
name: EntryClass::Recycled.into(),
|
name: EntryClass::Recycled.into(),
|
||||||
uuid: UUID_SCHEMA_CLASS_RECYCLED,
|
uuid: UUID_SCHEMA_CLASS_RECYCLED,
|
||||||
description: String::from("An object that has been deleted, but still recoverable via the revive operation. Recycled objects are not modifiable, only revivable."),
|
description: String::from("An object that has been deleted, but still recoverable via the revive operation. Recycled objects are not modifiable, only revivable."),
|
||||||
|
systemmay: vec![Attribute::RecycledDirectMemberOf.into()],
|
||||||
.. Default::default()
|
.. Default::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -2719,10 +2719,7 @@ mod tests {
|
||||||
EntryClass::OAuth2ResourceServerBasic.to_value()
|
EntryClass::OAuth2ResourceServerBasic.to_value()
|
||||||
),
|
),
|
||||||
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
||||||
(
|
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||||
Attribute::OAuth2RsName,
|
|
||||||
Value::new_iname("test_resource_server")
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
Attribute::DisplayName,
|
Attribute::DisplayName,
|
||||||
Value::new_utf8s("test_resource_server")
|
Value::new_utf8s("test_resource_server")
|
||||||
|
@ -2764,10 +2761,7 @@ mod tests {
|
||||||
EntryClass::OAuth2ResourceServerBasic.to_value()
|
EntryClass::OAuth2ResourceServerBasic.to_value()
|
||||||
),
|
),
|
||||||
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
(Attribute::Uuid, Value::Uuid(rs_uuid)),
|
||||||
(
|
(Attribute::Name, Value::new_iname("test_resource_server")),
|
||||||
Attribute::OAuth2RsName,
|
|
||||||
Value::new_iname("test_resource_server")
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
Attribute::DisplayName,
|
Attribute::DisplayName,
|
||||||
Value::new_utf8s("test_resource_server")
|
Value::new_utf8s("test_resource_server")
|
||||||
|
@ -2790,10 +2784,7 @@ mod tests {
|
||||||
EntryClass::OAuth2ResourceServerBasic.to_value()
|
EntryClass::OAuth2ResourceServerBasic.to_value()
|
||||||
),
|
),
|
||||||
(Attribute::Uuid, Value::Uuid(Uuid::new_v4())),
|
(Attribute::Uuid, Value::Uuid(Uuid::new_v4())),
|
||||||
(
|
(Attribute::Name, Value::new_iname("second_resource_server")),
|
||||||
Attribute::OAuth2RsName,
|
|
||||||
Value::new_iname("second_resource_server")
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
Attribute::DisplayName,
|
Attribute::DisplayName,
|
||||||
Value::new_utf8s("second_resource_server")
|
Value::new_utf8s("second_resource_server")
|
||||||
|
@ -2832,14 +2823,14 @@ mod tests {
|
||||||
|
|
||||||
let se_a = SearchEvent::new_impersonate_entry(
|
let se_a = SearchEvent::new_impersonate_entry(
|
||||||
E_TEST_ACCOUNT_1.clone(),
|
E_TEST_ACCOUNT_1.clone(),
|
||||||
filter_all!(f_pres(Attribute::OAuth2RsName)),
|
filter_all!(f_pres(Attribute::Name)),
|
||||||
);
|
);
|
||||||
let ex_a = vec![Arc::new(ev1)];
|
let ex_a = vec![Arc::new(ev1)];
|
||||||
let ex_a_reduced = vec![ev1_reduced];
|
let ex_a_reduced = vec![ev1_reduced];
|
||||||
|
|
||||||
let se_b = SearchEvent::new_impersonate_entry(
|
let se_b = SearchEvent::new_impersonate_entry(
|
||||||
E_TEST_ACCOUNT_2.clone(),
|
E_TEST_ACCOUNT_2.clone(),
|
||||||
filter_all!(f_pres(Attribute::OAuth2RsName)),
|
filter_all!(f_pres(Attribute::Name)),
|
||||||
);
|
);
|
||||||
let ex_b = vec![];
|
let ex_b = vec![];
|
||||||
|
|
||||||
|
|
|
@ -191,7 +191,7 @@ fn search_oauth2_filter_entry<'a>(
|
||||||
Attribute::Class.as_ref(),
|
Attribute::Class.as_ref(),
|
||||||
Attribute::DisplayName.as_ref(),
|
Attribute::DisplayName.as_ref(),
|
||||||
Attribute::Uuid.as_ref(),
|
Attribute::Uuid.as_ref(),
|
||||||
Attribute::OAuth2RsName.as_ref(),
|
Attribute::Name.as_ref(),
|
||||||
Attribute::OAuth2RsOrigin.as_ref(),
|
Attribute::OAuth2RsOrigin.as_ref(),
|
||||||
Attribute::OAuth2RsOriginLanding.as_ref(),
|
Attribute::OAuth2RsOriginLanding.as_ref(),
|
||||||
Attribute::Image.as_ref()
|
Attribute::Image.as_ref()
|
||||||
|
|
|
@ -204,6 +204,7 @@ mod tests {
|
||||||
|
|
||||||
let e1 = entry_init!(
|
let e1 = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(
|
(
|
||||||
|
@ -216,6 +217,7 @@ mod tests {
|
||||||
|
|
||||||
let e2 = entry_init!(
|
let e2 = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson2")),
|
(Attribute::Name, Value::new_iname("testperson2")),
|
||||||
(
|
(
|
||||||
|
@ -228,6 +230,7 @@ mod tests {
|
||||||
|
|
||||||
let e3 = entry_init!(
|
let e3 = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson3")),
|
(Attribute::Name, Value::new_iname("testperson3")),
|
||||||
(
|
(
|
||||||
|
|
|
@ -739,6 +739,84 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "info", skip_all)]
|
||||||
|
/// Migrations for Oauth to move rs name from a dedicated type to name
|
||||||
|
/// and to allow oauth2 sessions on resource servers for client credentials
|
||||||
|
/// grants. Accounts, persons and service accounts have some attributes
|
||||||
|
/// relocated to allow oauth2 rs to become accounts.
|
||||||
|
pub fn migrate_domain_4_to_5(&mut self) -> Result<(), OperationError> {
|
||||||
|
let idm_schema_classes = [
|
||||||
|
SCHEMA_CLASS_PERSON_DL5.clone().into(),
|
||||||
|
SCHEMA_CLASS_ACCOUNT_DL5.clone().into(),
|
||||||
|
SCHEMA_CLASS_SERVICE_ACCOUNT_DL5.clone().into(),
|
||||||
|
SCHEMA_CLASS_OAUTH2_RS_DL5.clone().into(),
|
||||||
|
SCHEMA_CLASS_OAUTH2_RS_BASIC_DL5.clone().into(),
|
||||||
|
IDM_ACP_OAUTH2_MANAGE_DL5.clone().into(),
|
||||||
|
];
|
||||||
|
|
||||||
|
idm_schema_classes
|
||||||
|
.into_iter()
|
||||||
|
.try_for_each(|entry| self.internal_migrate_or_create(entry))
|
||||||
|
.map_err(|err| {
|
||||||
|
error!(?err, "migrate_domain_4_to_5 -> Error");
|
||||||
|
err
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Reload mid txn so that the next modification works.
|
||||||
|
self.force_schema_reload();
|
||||||
|
self.reload()?;
|
||||||
|
|
||||||
|
// Now we remove attributes from service accounts that have been unable to be set
|
||||||
|
// via a user interface for more than a year.
|
||||||
|
let filter = filter!(f_and!([
|
||||||
|
f_eq(Attribute::Class, EntryClass::Account.into()),
|
||||||
|
f_eq(Attribute::Class, EntryClass::ServiceAccount.into()),
|
||||||
|
]));
|
||||||
|
let modlist = ModifyList::new_list(vec![
|
||||||
|
Modify::Purged(Attribute::PassKeys.into()),
|
||||||
|
Modify::Purged(Attribute::AttestedPasskeys.into()),
|
||||||
|
Modify::Purged(Attribute::CredentialUpdateIntentToken.into()),
|
||||||
|
Modify::Purged(Attribute::RadiusSecret.into()),
|
||||||
|
]);
|
||||||
|
self.internal_modify(&filter, &modlist)?;
|
||||||
|
|
||||||
|
// Now move all oauth2 rs name.
|
||||||
|
let filter = filter!(f_eq(
|
||||||
|
Attribute::Class,
|
||||||
|
EntryClass::OAuth2ResourceServer.into()
|
||||||
|
));
|
||||||
|
|
||||||
|
let pre_candidates = self.internal_search(filter).map_err(|err| {
|
||||||
|
admin_error!(?err, "migrate_domain_4_to_5 internal search failure");
|
||||||
|
err
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let modset: Vec<_> = pre_candidates
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|ent| {
|
||||||
|
ent.get_ava_single_iname(Attribute::OAuth2RsName)
|
||||||
|
.map(|rs_name| {
|
||||||
|
let modlist = vec![
|
||||||
|
Modify::Present(Attribute::Class.into(), EntryClass::Account.into()),
|
||||||
|
Modify::Present(Attribute::Name.into(), Value::new_iname(rs_name)),
|
||||||
|
m_purge(Attribute::OAuth2RsName),
|
||||||
|
];
|
||||||
|
|
||||||
|
(ent.get_uuid(), ModifyList::new_list(modlist))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// If there is nothing, we don't need to do anything.
|
||||||
|
if modset.is_empty() {
|
||||||
|
admin_info!("migrate_domain_4_to_5 no entries to migrate, complete");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the batch mod.
|
||||||
|
self.internal_batch_modify(modset.into_iter())
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(level = "info", skip_all)]
|
#[instrument(level = "info", skip_all)]
|
||||||
pub fn initialise_schema_core(&mut self) -> Result<(), OperationError> {
|
pub fn initialise_schema_core(&mut self) -> Result<(), OperationError> {
|
||||||
admin_debug!("initialise_schema_core -> start ...");
|
admin_debug!("initialise_schema_core -> start ...");
|
||||||
|
|
|
@ -1159,9 +1159,9 @@ impl QueryServer {
|
||||||
|
|
||||||
let d_info = Arc::new(CowCell::new(DomainInfo {
|
let d_info = Arc::new(CowCell::new(DomainInfo {
|
||||||
d_uuid,
|
d_uuid,
|
||||||
// Start with our minimum supported level.
|
// Start with our level as zero.
|
||||||
// This will be reloaded from the DB shortly :)
|
// This will be reloaded from the DB shortly :)
|
||||||
d_vers: DOMAIN_MIN_LEVEL,
|
d_vers: DOMAIN_LEVEL_0,
|
||||||
d_name: domain_name.clone(),
|
d_name: domain_name.clone(),
|
||||||
// we set the domain_display_name to the configuration file's domain_name
|
// we set the domain_display_name to the configuration file's domain_name
|
||||||
// here because the database is not started, so we cannot pull it from there.
|
// here because the database is not started, so we cannot pull it from there.
|
||||||
|
@ -1630,6 +1630,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
self.migrate_domain_3_to_4()?;
|
self.migrate_domain_3_to_4()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if previous_version <= DOMAIN_LEVEL_4 && domain_info_version >= DOMAIN_LEVEL_5 {
|
||||||
|
self.migrate_domain_4_to_5()?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1853,6 +1857,7 @@ mod tests {
|
||||||
assert!(server_txn
|
assert!(server_txn
|
||||||
.internal_create(vec![entry_init!(
|
.internal_create(vec![entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
(Attribute::Uuid, Value::Uuid(t_uuid)),
|
||||||
|
@ -1983,6 +1988,7 @@ mod tests {
|
||||||
let mut server_txn = server.write(duration_from_epoch_now()).await;
|
let mut server_txn = server.write(duration_from_epoch_now()).await;
|
||||||
let e1 = entry_init!(
|
let e1 = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(
|
(
|
||||||
|
|
|
@ -209,9 +209,21 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
if !self.changed_oauth2 {
|
if !self.changed_oauth2 {
|
||||||
self.changed_oauth2 = norm_cand
|
self.changed_oauth2 = norm_cand
|
||||||
.iter()
|
.iter()
|
||||||
.chain(pre_candidates.iter().map(|e| e.as_ref()))
|
.zip(pre_candidates.iter().map(|e| e.as_ref()))
|
||||||
.any(|e| {
|
.any(|(post, pre)| {
|
||||||
e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into())
|
// This is in the modify path only - because sessions can update the RS
|
||||||
|
// this can trigger reloads of all the oauth2 clients. That would make
|
||||||
|
// client credentials grant pretty expensive in these cases. To avoid this
|
||||||
|
// we check if "anything else" beside the oauth2session changed in this
|
||||||
|
// txn.
|
||||||
|
(post.attribute_equality(
|
||||||
|
Attribute::Class,
|
||||||
|
&EntryClass::OAuth2ResourceServer.into(),
|
||||||
|
) || pre.attribute_equality(
|
||||||
|
Attribute::Class,
|
||||||
|
&EntryClass::OAuth2ResourceServer.into(),
|
||||||
|
)) && post
|
||||||
|
.entry_changed_excluding_attribute(Attribute::OAuth2Session, &self.cid)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if !self.changed_domain {
|
if !self.changed_domain {
|
||||||
|
@ -511,6 +523,7 @@ mod tests {
|
||||||
|
|
||||||
let e1 = entry_init!(
|
let e1 = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(
|
(
|
||||||
|
@ -523,6 +536,7 @@ mod tests {
|
||||||
|
|
||||||
let e2 = entry_init!(
|
let e2 = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson2")),
|
(Attribute::Name, Value::new_iname("testperson2")),
|
||||||
(
|
(
|
||||||
|
@ -671,6 +685,7 @@ mod tests {
|
||||||
|
|
||||||
let e1 = entry_init!(
|
let e1 = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(
|
(
|
||||||
|
|
|
@ -148,7 +148,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
// Get this entries uuid.
|
// Get this entries uuid.
|
||||||
let u: Uuid = e.get_uuid();
|
let u: Uuid = e.get_uuid();
|
||||||
|
|
||||||
if let Some(riter) = e.get_ava_as_refuuid(Attribute::DirectMemberOf) {
|
if let Some(riter) = e.get_ava_as_refuuid(Attribute::RecycledDirectMemberOf) {
|
||||||
for g_uuid in riter {
|
for g_uuid in riter {
|
||||||
dm_mods
|
dm_mods
|
||||||
.entry(g_uuid)
|
.entry(g_uuid)
|
||||||
|
@ -291,6 +291,7 @@ mod tests {
|
||||||
// Create some recycled objects
|
// Create some recycled objects
|
||||||
let e1 = entry_init!(
|
let e1 = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(
|
(
|
||||||
|
@ -303,6 +304,7 @@ mod tests {
|
||||||
|
|
||||||
let e2 = entry_init!(
|
let e2 = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson2")),
|
(Attribute::Name, Value::new_iname("testperson2")),
|
||||||
(
|
(
|
||||||
|
@ -397,6 +399,7 @@ mod tests {
|
||||||
|
|
||||||
let e1 = entry_init!(
|
let e1 = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(
|
(
|
||||||
|
@ -532,6 +535,7 @@ mod tests {
|
||||||
// First, create an entry, then push it through the lifecycle.
|
// First, create an entry, then push it through the lifecycle.
|
||||||
let e_ts = entry_init!(
|
let e_ts = entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname("testperson1")),
|
(Attribute::Name, Value::new_iname("testperson1")),
|
||||||
(
|
(
|
||||||
|
@ -610,6 +614,7 @@ mod tests {
|
||||||
fn create_user(name: &str, uuid: &str) -> Entry<EntryInit, EntryNew> {
|
fn create_user(name: &str, uuid: &str) -> Entry<EntryInit, EntryNew> {
|
||||||
entry_init!(
|
entry_init!(
|
||||||
(Attribute::Class, EntryClass::Object.to_value()),
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(Attribute::Class, EntryClass::Account.to_value()),
|
||||||
(Attribute::Class, EntryClass::Person.to_value()),
|
(Attribute::Class, EntryClass::Person.to_value()),
|
||||||
(Attribute::Name, Value::new_iname(name)),
|
(Attribute::Name, Value::new_iname(name)),
|
||||||
(
|
(
|
||||||
|
@ -639,7 +644,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_entry_has_mo(qs: &mut QueryServerWriteTransaction, name: &str, mo: &str) -> bool {
|
fn check_entry_has_mo(qs: &mut QueryServerWriteTransaction, name: &str, mo: &str) -> bool {
|
||||||
let e = qs
|
let entry = qs
|
||||||
.internal_search(filter!(f_eq(
|
.internal_search(filter!(f_eq(
|
||||||
Attribute::Name,
|
Attribute::Name,
|
||||||
PartialValue::new_iname(name)
|
PartialValue::new_iname(name)
|
||||||
|
@ -648,7 +653,9 @@ mod tests {
|
||||||
.pop()
|
.pop()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
e.attribute_equality(Attribute::MemberOf, &PartialValue::new_refer_s(mo).unwrap())
|
trace!(?entry);
|
||||||
|
|
||||||
|
entry.attribute_equality(Attribute::MemberOf, &PartialValue::new_refer_s(mo).unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[qs_test]
|
#[qs_test]
|
||||||
|
|
|
@ -1030,8 +1030,7 @@ impl From<OauthClaimMapJoin> for DbValueOauthClaimMapJoinV1 {
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct Oauth2Session {
|
pub struct Oauth2Session {
|
||||||
pub parent: Uuid,
|
pub parent: Option<Uuid>,
|
||||||
// pub expiry: Option<OffsetDateTime>,
|
|
||||||
pub state: SessionState,
|
pub state: SessionState,
|
||||||
pub issued_at: OffsetDateTime,
|
pub issued_at: OffsetDateTime,
|
||||||
pub rs_uuid: Uuid,
|
pub rs_uuid: Uuid,
|
||||||
|
|
|
@ -779,6 +779,8 @@ impl ValueSetOauth2Session {
|
||||||
.map(SessionState::ExpiresAt)
|
.map(SessionState::ExpiresAt)
|
||||||
.unwrap_or(SessionState::NeverExpires);
|
.unwrap_or(SessionState::NeverExpires);
|
||||||
|
|
||||||
|
let parent = Some(parent);
|
||||||
|
|
||||||
// Insert to the rs_filter.
|
// Insert to the rs_filter.
|
||||||
rs_filter |= rs_uuid.as_u128();
|
rs_filter |= rs_uuid.as_u128();
|
||||||
Some((
|
Some((
|
||||||
|
@ -833,6 +835,8 @@ impl ValueSetOauth2Session {
|
||||||
|
|
||||||
rs_filter |= rs_uuid.as_u128();
|
rs_filter |= rs_uuid.as_u128();
|
||||||
|
|
||||||
|
let parent = Some(parent);
|
||||||
|
|
||||||
Some((
|
Some((
|
||||||
refer,
|
refer,
|
||||||
Oauth2Session {
|
Oauth2Session {
|
||||||
|
@ -842,7 +846,59 @@ impl ValueSetOauth2Session {
|
||||||
rs_uuid,
|
rs_uuid,
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
|
} // End V2
|
||||||
|
DbValueOauth2Session::V3 {
|
||||||
|
refer,
|
||||||
|
parent,
|
||||||
|
state,
|
||||||
|
issued_at,
|
||||||
|
rs_uuid,
|
||||||
|
} => {
|
||||||
|
// Convert things.
|
||||||
|
let issued_at = OffsetDateTime::parse(&issued_at, &Rfc3339)
|
||||||
|
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
|
||||||
|
.map_err(|e| {
|
||||||
|
admin_error!(
|
||||||
|
?e,
|
||||||
|
"Invalidating session {} due to invalid issued_at timestamp",
|
||||||
|
refer
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let state = match state {
|
||||||
|
DbValueSessionStateV1::ExpiresAt(e_inner) => {
|
||||||
|
OffsetDateTime::parse(&e_inner, &Rfc3339)
|
||||||
|
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
|
||||||
|
.map(SessionState::ExpiresAt)
|
||||||
|
.map_err(|e| {
|
||||||
|
admin_error!(
|
||||||
|
?e,
|
||||||
|
"Invalidating session {} due to invalid expiry timestamp",
|
||||||
|
refer
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.ok()?
|
||||||
}
|
}
|
||||||
|
DbValueSessionStateV1::Never => SessionState::NeverExpires,
|
||||||
|
DbValueSessionStateV1::RevokedAt(dc) => SessionState::RevokedAt(Cid {
|
||||||
|
s_uuid: dc.server_id,
|
||||||
|
ts: dc.timestamp,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
rs_filter |= rs_uuid.as_u128();
|
||||||
|
|
||||||
|
Some((
|
||||||
|
refer,
|
||||||
|
Oauth2Session {
|
||||||
|
parent,
|
||||||
|
state,
|
||||||
|
issued_at,
|
||||||
|
rs_uuid,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
} // End V3
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
@ -1096,7 +1152,7 @@ impl ValueSetT for ValueSetOauth2Session {
|
||||||
DbValueSetV2::Oauth2Session(
|
DbValueSetV2::Oauth2Session(
|
||||||
self.map
|
self.map
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(u, m)| DbValueOauth2Session::V2 {
|
.map(|(u, m)| DbValueOauth2Session::V3 {
|
||||||
refer: *u,
|
refer: *u,
|
||||||
parent: m.parent,
|
parent: m.parent,
|
||||||
state: match &m.state {
|
state: match &m.state {
|
||||||
|
@ -1936,7 +1992,7 @@ mod tests {
|
||||||
Oauth2Session {
|
Oauth2Session {
|
||||||
state: SessionState::NeverExpires,
|
state: SessionState::NeverExpires,
|
||||||
issued_at: OffsetDateTime::now_utc(),
|
issued_at: OffsetDateTime::now_utc(),
|
||||||
parent: Uuid::new_v4(),
|
parent: Some(Uuid::new_v4()),
|
||||||
rs_uuid: Uuid::new_v4(),
|
rs_uuid: Uuid::new_v4(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1966,7 +2022,7 @@ mod tests {
|
||||||
Oauth2Session {
|
Oauth2Session {
|
||||||
state: SessionState::NeverExpires,
|
state: SessionState::NeverExpires,
|
||||||
issued_at: OffsetDateTime::now_utc(),
|
issued_at: OffsetDateTime::now_utc(),
|
||||||
parent: Uuid::new_v4(),
|
parent: Some(Uuid::new_v4()),
|
||||||
rs_uuid: Uuid::new_v4(),
|
rs_uuid: Uuid::new_v4(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1976,7 +2032,7 @@ mod tests {
|
||||||
Oauth2Session {
|
Oauth2Session {
|
||||||
state: SessionState::RevokedAt(zero_cid.clone()),
|
state: SessionState::RevokedAt(zero_cid.clone()),
|
||||||
issued_at: OffsetDateTime::now_utc(),
|
issued_at: OffsetDateTime::now_utc(),
|
||||||
parent: Uuid::new_v4(),
|
parent: Some(Uuid::new_v4()),
|
||||||
rs_uuid: Uuid::new_v4(),
|
rs_uuid: Uuid::new_v4(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -2001,7 +2057,7 @@ mod tests {
|
||||||
Oauth2Session {
|
Oauth2Session {
|
||||||
state: SessionState::NeverExpires,
|
state: SessionState::NeverExpires,
|
||||||
issued_at: OffsetDateTime::now_utc(),
|
issued_at: OffsetDateTime::now_utc(),
|
||||||
parent: Uuid::new_v4(),
|
parent: Some(Uuid::new_v4()),
|
||||||
rs_uuid: Uuid::new_v4(),
|
rs_uuid: Uuid::new_v4(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -2011,7 +2067,7 @@ mod tests {
|
||||||
Oauth2Session {
|
Oauth2Session {
|
||||||
state: SessionState::RevokedAt(zero_cid.clone()),
|
state: SessionState::RevokedAt(zero_cid.clone()),
|
||||||
issued_at: OffsetDateTime::now_utc(),
|
issued_at: OffsetDateTime::now_utc(),
|
||||||
parent: Uuid::new_v4(),
|
parent: Some(Uuid::new_v4()),
|
||||||
rs_uuid: Uuid::new_v4(),
|
rs_uuid: Uuid::new_v4(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -2039,7 +2095,7 @@ mod tests {
|
||||||
Oauth2Session {
|
Oauth2Session {
|
||||||
state: SessionState::NeverExpires,
|
state: SessionState::NeverExpires,
|
||||||
issued_at: OffsetDateTime::now_utc(),
|
issued_at: OffsetDateTime::now_utc(),
|
||||||
parent: Uuid::new_v4(),
|
parent: Some(Uuid::new_v4()),
|
||||||
rs_uuid: Uuid::new_v4(),
|
rs_uuid: Uuid::new_v4(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -2050,7 +2106,7 @@ mod tests {
|
||||||
Oauth2Session {
|
Oauth2Session {
|
||||||
state: SessionState::RevokedAt(one_cid.clone()),
|
state: SessionState::RevokedAt(one_cid.clone()),
|
||||||
issued_at: OffsetDateTime::now_utc(),
|
issued_at: OffsetDateTime::now_utc(),
|
||||||
parent: Uuid::new_v4(),
|
parent: Some(Uuid::new_v4()),
|
||||||
rs_uuid: Uuid::new_v4(),
|
rs_uuid: Uuid::new_v4(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -2059,7 +2115,7 @@ mod tests {
|
||||||
Oauth2Session {
|
Oauth2Session {
|
||||||
state: SessionState::RevokedAt(zero_cid.clone()),
|
state: SessionState::RevokedAt(zero_cid.clone()),
|
||||||
issued_at: OffsetDateTime::now_utc(),
|
issued_at: OffsetDateTime::now_utc(),
|
||||||
parent: Uuid::new_v4(),
|
parent: Some(Uuid::new_v4()),
|
||||||
rs_uuid: Uuid::new_v4(),
|
rs_uuid: Uuid::new_v4(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -2093,7 +2149,7 @@ mod tests {
|
||||||
Oauth2Session {
|
Oauth2Session {
|
||||||
state: SessionState::NeverExpires,
|
state: SessionState::NeverExpires,
|
||||||
issued_at: OffsetDateTime::now_utc(),
|
issued_at: OffsetDateTime::now_utc(),
|
||||||
parent: Uuid::new_v4(),
|
parent: Some(Uuid::new_v4()),
|
||||||
rs_uuid: Uuid::new_v4(),
|
rs_uuid: Uuid::new_v4(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -2104,7 +2160,7 @@ mod tests {
|
||||||
Oauth2Session {
|
Oauth2Session {
|
||||||
state: SessionState::RevokedAt(one_cid.clone()),
|
state: SessionState::RevokedAt(one_cid.clone()),
|
||||||
issued_at: OffsetDateTime::now_utc(),
|
issued_at: OffsetDateTime::now_utc(),
|
||||||
parent: Uuid::new_v4(),
|
parent: Some(Uuid::new_v4()),
|
||||||
rs_uuid: Uuid::new_v4(),
|
rs_uuid: Uuid::new_v4(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -2113,7 +2169,7 @@ mod tests {
|
||||||
Oauth2Session {
|
Oauth2Session {
|
||||||
state: SessionState::RevokedAt(zero_cid.clone()),
|
state: SessionState::RevokedAt(zero_cid.clone()),
|
||||||
issued_at: OffsetDateTime::now_utc(),
|
issued_at: OffsetDateTime::now_utc(),
|
||||||
parent: Uuid::new_v4(),
|
parent: Some(Uuid::new_v4()),
|
||||||
rs_uuid: Uuid::new_v4(),
|
rs_uuid: Uuid::new_v4(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -2151,7 +2207,7 @@ mod tests {
|
||||||
Oauth2Session {
|
Oauth2Session {
|
||||||
state: SessionState::RevokedAt(zero_cid),
|
state: SessionState::RevokedAt(zero_cid),
|
||||||
issued_at: OffsetDateTime::now_utc(),
|
issued_at: OffsetDateTime::now_utc(),
|
||||||
parent: Uuid::new_v4(),
|
parent: Some(Uuid::new_v4()),
|
||||||
rs_uuid: Uuid::new_v4(),
|
rs_uuid: Uuid::new_v4(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -2160,7 +2216,7 @@ mod tests {
|
||||||
Oauth2Session {
|
Oauth2Session {
|
||||||
state: SessionState::RevokedAt(one_cid),
|
state: SessionState::RevokedAt(one_cid),
|
||||||
issued_at: OffsetDateTime::now_utc(),
|
issued_at: OffsetDateTime::now_utc(),
|
||||||
parent: Uuid::new_v4(),
|
parent: Some(Uuid::new_v4()),
|
||||||
rs_uuid: Uuid::new_v4(),
|
rs_uuid: Uuid::new_v4(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -2169,7 +2225,7 @@ mod tests {
|
||||||
Oauth2Session {
|
Oauth2Session {
|
||||||
state: SessionState::RevokedAt(two_cid.clone()),
|
state: SessionState::RevokedAt(two_cid.clone()),
|
||||||
issued_at: OffsetDateTime::now_utc(),
|
issued_at: OffsetDateTime::now_utc(),
|
||||||
parent: Uuid::new_v4(),
|
parent: Some(Uuid::new_v4()),
|
||||||
rs_uuid: Uuid::new_v4(),
|
rs_uuid: Uuid::new_v4(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#![deny(warnings)]
|
#![deny(warnings)]
|
||||||
use std::collections::HashMap;
|
use std::collections::{BTreeSet, HashMap};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
@ -346,7 +346,7 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
|
||||||
|
|
||||||
let response = client
|
let response = client
|
||||||
.post(rsclient.make_url("/oauth2/token/introspect"))
|
.post(rsclient.make_url("/oauth2/token/introspect"))
|
||||||
.basic_auth("test_integration", Some(client_secret))
|
.basic_auth("test_integration", Some(client_secret.clone()))
|
||||||
.form(&intr_request)
|
.form(&intr_request)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
|
@ -415,6 +415,59 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
|
||||||
|
|
||||||
assert!(userinfo == oidc);
|
assert!(userinfo == oidc);
|
||||||
|
|
||||||
|
// Step 6 - Show that our client can perform a client credentials grant
|
||||||
|
|
||||||
|
let form_req: AccessTokenRequest = GrantTypeReq::ClientCredentials {
|
||||||
|
scope: Some(BTreeSet::from([
|
||||||
|
"email".to_string(),
|
||||||
|
"read".to_string(),
|
||||||
|
"openid".to_string(),
|
||||||
|
])),
|
||||||
|
}
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(rsclient.make_url("/oauth2/token"))
|
||||||
|
.basic_auth("test_integration", Some(client_secret.clone()))
|
||||||
|
.form(&form_req)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to send client credentials request.");
|
||||||
|
|
||||||
|
assert!(response.status() == reqwest::StatusCode::OK);
|
||||||
|
|
||||||
|
let atr = response
|
||||||
|
.json::<AccessTokenResponse>()
|
||||||
|
.await
|
||||||
|
.expect("Unable to decode AccessTokenResponse");
|
||||||
|
|
||||||
|
// Step 7 - inspect the granted client credentials token.
|
||||||
|
let intr_request = AccessTokenIntrospectRequest {
|
||||||
|
token: atr.access_token.clone(),
|
||||||
|
token_type_hint: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(rsclient.make_url("/oauth2/token/introspect"))
|
||||||
|
.basic_auth("test_integration", Some(client_secret))
|
||||||
|
.form(&intr_request)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to send token introspect request.");
|
||||||
|
|
||||||
|
assert!(response.status() == reqwest::StatusCode::OK);
|
||||||
|
|
||||||
|
let tir = response
|
||||||
|
.json::<AccessTokenIntrospectResponse>()
|
||||||
|
.await
|
||||||
|
.expect("Unable to decode AccessTokenIntrospectResponse");
|
||||||
|
|
||||||
|
assert!(tir.active);
|
||||||
|
assert!(tir.scope.is_some());
|
||||||
|
assert!(tir.client_id.as_deref() == Some("test_integration"));
|
||||||
|
assert!(tir.username.as_deref() == Some("test_integration@localhost"));
|
||||||
|
assert!(tir.token_type.as_deref() == Some("access_token"));
|
||||||
|
|
||||||
// auth back with admin so we can test deleting things
|
// auth back with admin so we can test deleting things
|
||||||
let res = rsclient
|
let res = rsclient
|
||||||
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
|
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
|
||||||
|
|
Loading…
Reference in a new issue