mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
613 oauth2 logout (#1184)
* Oauth2 sessions * Start to add session consistency * Add tests for session consistency. * Session refint works! * Add support for oauth2 session removal
This commit is contained in:
parent
69b9c5845a
commit
06c9e087cb
|
@ -1,9 +1,9 @@
|
|||
# OAuth2
|
||||
|
||||
OAuth is a web authorisation protocol that allows "single sign on". It's key to note
|
||||
OAuth is authorisation, not authentication, as the protocol in its default forms
|
||||
do not provide identity or authentication information, only information that
|
||||
an entity is authorised for the requested resources.
|
||||
OAuth only provides authorisation, as the protocol in its default forms
|
||||
do not provide identity or authentication information. All that Oauth2 provides is
|
||||
information that an entity is authorised for the requested resources.
|
||||
|
||||
OAuth can tie into extensions allowing an identity provider to reveal information
|
||||
about authorised sessions. This extends OAuth from an authorisation only system
|
||||
|
@ -60,7 +60,8 @@ Kanidm will expose its OAuth2 APIs at the following URLs:
|
|||
* user auth url: https://idm.example.com/ui/oauth2
|
||||
* api auth url: https://idm.example.com/oauth2/authorise
|
||||
* token url: https://idm.example.com/oauth2/token
|
||||
* token inspect url: https://idm.example.com/oauth2/inspect
|
||||
* rfc7662 token introspection url: https://idm.example.com/oauth2/token/introspect
|
||||
* rfc7009 token revoke url: https://idm.example.com/oauth2/token/revoke
|
||||
|
||||
OpenID Connect discovery - you need to substitute your OAuth2 client id in the following
|
||||
urls:
|
||||
|
@ -87,11 +88,11 @@ The first is scope mappings. These provide a set of scopes if a user is a member
|
|||
group within Kanidm. This allows you to create a relationship between the scopes of a resource
|
||||
server, and the groups/roles in Kanidm which can be specific to that resource server.
|
||||
|
||||
For an authorisation to proceed, all scopes requested must be available in the final scope set
|
||||
that is granted to the account.
|
||||
For an authorisation to proceed, all scopes requested by the resource server must be available in the
|
||||
final scope set that is granted to the account.
|
||||
|
||||
The second is supplemental scope mappings. These function the same as scope maps where membership
|
||||
of a group provides a set of scopes to the account, however these scopes are NOT consulted during
|
||||
of a group provides a set of scopes to the account. However these scopes are NOT consulted during
|
||||
authorisation decisions made by Kanidm. These scopes exists to allow optional properties to be
|
||||
provided (such as personal information about a subset of accounts to be revealed) or so that the resource server
|
||||
may make it's own authorisation decisions based on the provided scopes.
|
||||
|
@ -184,6 +185,13 @@ Not all resource servers support modern standards like PKCE or ECDSA. In these s
|
|||
it may be necessary to disable these on a per-resource server basis. Disabling these on
|
||||
one resource server will not affect others.
|
||||
|
||||
{{#template
|
||||
templates/kani-warning.md
|
||||
imagepath=images
|
||||
title=WARNING
|
||||
text=Changing these settings MAY have serious consequences on the security of your resource server. You should avoid changing these if at all possible!
|
||||
}}
|
||||
|
||||
To disable PKCE for a resource server:
|
||||
|
||||
kanidm system oauth2 warning_insecure_client_disable_pkce <resource server name>
|
||||
|
|
|
@ -94,11 +94,16 @@ pub struct AccessTokenRequest {
|
|||
pub code_verifier: Option<String>,
|
||||
}
|
||||
|
||||
// We now check code_verifier is the same via the formula.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct TokenRevokeRequest {
|
||||
pub token: String,
|
||||
/// Generally not needed. See:
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc7009#section-4.1.2>
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub token_type_hint: Option<String>,
|
||||
}
|
||||
|
||||
// If and only if it checks out, we proceed.
|
||||
|
||||
// Returned as a json body
|
||||
// The corresponding Response to a revoke request is empty body with 200.
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct AccessTokenResponse {
|
||||
|
|
|
@ -24,6 +24,7 @@ use kanidmd_lib::{
|
|||
},
|
||||
idm::delayed::DelayedAction,
|
||||
idm::event::{GeneratePasswordEvent, RegenerateRadiusSecretEvent, UnixPasswordChangeEvent},
|
||||
idm::oauth2::{Oauth2Error, TokenRevokeRequest},
|
||||
idm::server::{IdmServer, IdmServerTransaction},
|
||||
idm::serviceaccount::{DestroyApiTokenEvent, GenerateApiTokenEvent},
|
||||
modify::{Modify, ModifyInvalid, ModifyList},
|
||||
|
@ -1351,8 +1352,8 @@ impl QueryServerWriteV1 {
|
|||
filter: Filter<FilterInvalid>,
|
||||
eventid: Uuid,
|
||||
) -> Result<(), OperationError> {
|
||||
let mut idms_prox_write = self.idms.proxy_write(duration_from_epoch_now()).await;
|
||||
let ct = duration_from_epoch_now();
|
||||
let mut idms_prox_write = self.idms.proxy_write(ct).await;
|
||||
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
|
@ -1392,6 +1393,24 @@ impl QueryServerWriteV1 {
|
|||
.and_then(|_| idms_prox_write.commit().map(|_| ()))
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn handle_oauth2_token_revoke(
|
||||
&self,
|
||||
client_authz: String,
|
||||
intr_req: TokenRevokeRequest,
|
||||
eventid: Uuid,
|
||||
) -> Result<(), Oauth2Error> {
|
||||
let ct = duration_from_epoch_now();
|
||||
let mut idms_prox_write = self.idms.proxy_write(ct).await;
|
||||
idms_prox_write
|
||||
.oauth2_token_revoke(&client_authz, &intr_req, ct)
|
||||
.and_then(|()| idms_prox_write.commit().map_err(Oauth2Error::ServerError))
|
||||
}
|
||||
|
||||
// ===== These below are internal only event types. =====
|
||||
#[instrument(
|
||||
level = "info",
|
||||
|
|
|
@ -2,7 +2,7 @@ use kanidm_proto::oauth2::AuthorisationResponse;
|
|||
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||
use kanidmd_lib::idm::oauth2::{
|
||||
AccessTokenIntrospectRequest, AccessTokenRequest, AuthorisationRequest, AuthorisePermitSuccess,
|
||||
AuthoriseResponse, ErrorResponse, Oauth2Error,
|
||||
AuthoriseResponse, ErrorResponse, Oauth2Error, TokenRevokeRequest,
|
||||
};
|
||||
use kanidmd_lib::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -723,6 +723,68 @@ pub async fn oauth2_token_introspect_post(mut req: tide::Request<AppState>) -> t
|
|||
})
|
||||
}
|
||||
|
||||
pub async fn oauth2_token_revoke_post(mut req: tide::Request<AppState>) -> tide::Result {
|
||||
// This is called directly by the resource server, where we then revoke
|
||||
// the token identified by this request.
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let client_authz = req
|
||||
.header("authorization")
|
||||
.and_then(|hv| hv.get(0))
|
||||
.and_then(|h| h.as_str().strip_prefix("Basic "))
|
||||
.map(str::to_string)
|
||||
.ok_or_else(|| {
|
||||
error!("Basic Authentication Not Provided");
|
||||
tide::Error::from_str(
|
||||
tide::StatusCode::Unauthorized,
|
||||
"Invalid Basic Authorisation",
|
||||
)
|
||||
})?;
|
||||
|
||||
// Get the introspection request, could we accept json or form? Prob needs content type here.
|
||||
let intr_req: TokenRevokeRequest = req.body_form().await.map_err(|e| {
|
||||
request_error!("{:?}", e);
|
||||
tide::Error::from_str(
|
||||
tide::StatusCode::BadRequest,
|
||||
"Invalid Oauth2 TokenRevokeRequest",
|
||||
)
|
||||
})?;
|
||||
|
||||
request_trace!("Revoke Request - {:?}", intr_req);
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
.qe_w_ref
|
||||
.handle_oauth2_token_revoke(client_authz, intr_req, eventid)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(()) => Ok(tide::Response::new(200)),
|
||||
Err(Oauth2Error::AuthenticationRequired) => {
|
||||
// This will trigger our ui to auth and retry.
|
||||
Ok(tide::Response::new(tide::StatusCode::Unauthorized))
|
||||
}
|
||||
Err(e) => {
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
||||
let err = ErrorResponse {
|
||||
error: e.to_string(),
|
||||
error_description: None,
|
||||
error_uri: None,
|
||||
};
|
||||
|
||||
let mut res = tide::Response::new(400);
|
||||
tide::Body::from_json(&err).map(|b| {
|
||||
res.set_body(b);
|
||||
res
|
||||
})
|
||||
}
|
||||
}
|
||||
.map(|mut res| {
|
||||
res.insert_header("X-KANIDM-OPID", hvalue);
|
||||
res
|
||||
})
|
||||
}
|
||||
|
||||
pub fn oauth2_route_setup(appserver: &mut tide::Route<'_, AppState>, routemap: &mut RouteMap) {
|
||||
let mut oauth2_process = appserver.at("/oauth2");
|
||||
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
|
||||
|
@ -738,36 +800,45 @@ pub fn oauth2_route_setup(appserver: &mut tide::Route<'_, AppState>, routemap: &
|
|||
.at("/authorise/permit")
|
||||
.mapped_post(routemap, oauth2_authorise_permit_post)
|
||||
.mapped_get(routemap, oauth2_authorise_permit_get);
|
||||
|
||||
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
|
||||
// IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
|
||||
oauth2_process
|
||||
.at("/authorise/reject")
|
||||
.mapped_post(routemap, oauth2_authorise_reject_post)
|
||||
.mapped_get(routemap, oauth2_authorise_reject_get);
|
||||
|
||||
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
|
||||
// IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
|
||||
oauth2_process
|
||||
.at("/token")
|
||||
.mapped_post(routemap, oauth2_token_post);
|
||||
|
||||
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
|
||||
// IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
|
||||
oauth2_process
|
||||
.at("/token/introspect")
|
||||
.mapped_post(routemap, oauth2_token_introspect_post);
|
||||
oauth2_process
|
||||
.at("/token/revoke")
|
||||
.mapped_post(routemap, oauth2_token_revoke_post);
|
||||
|
||||
let mut openid_process = appserver.at("/oauth2/openid");
|
||||
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
|
||||
// IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
|
||||
|
||||
openid_process
|
||||
.at("/:client_id/.well-known/openid-configuration")
|
||||
.mapped_get(routemap, oauth2_openid_discovery_get);
|
||||
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
|
||||
// IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
|
||||
|
||||
openid_process
|
||||
.at("/:client_id/userinfo")
|
||||
.mapped_get(routemap, oauth2_openid_userinfo_get);
|
||||
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
|
||||
// IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
|
||||
|
||||
openid_process
|
||||
.at("/:client_id/public_key.jwk")
|
||||
.mapped_get(routemap, oauth2_openid_publickey_get);
|
||||
|
|
|
@ -399,6 +399,22 @@ pub enum DbValueSession {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum DbValueOauth2Session {
|
||||
V1 {
|
||||
#[serde(rename = "u")]
|
||||
refer: Uuid,
|
||||
#[serde(rename = "p")]
|
||||
parent: Uuid,
|
||||
#[serde(rename = "e")]
|
||||
expiry: Option<String>,
|
||||
#[serde(rename = "i")]
|
||||
issued_at: String,
|
||||
#[serde(rename = "r")]
|
||||
rs_uuid: Uuid,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum DbValueV1 {
|
||||
#[serde(rename = "U8")]
|
||||
|
@ -532,6 +548,8 @@ pub enum DbValueSetV2 {
|
|||
JwsKeyEs256(Vec<Vec<u8>>),
|
||||
#[serde(rename = "JR")]
|
||||
JwsKeyRs256(Vec<Vec<u8>>),
|
||||
#[serde(rename = "AS")]
|
||||
Oauth2Session(Vec<DbValueOauth2Session>),
|
||||
}
|
||||
|
||||
impl DbValueSetV2 {
|
||||
|
@ -568,6 +586,7 @@ impl DbValueSetV2 {
|
|||
DbValueSetV2::DeviceKey(set) => set.len(),
|
||||
DbValueSetV2::TrustedDeviceEnrollment(set) => set.len(),
|
||||
DbValueSetV2::Session(set) => set.len(),
|
||||
DbValueSetV2::Oauth2Session(set) => set.len(),
|
||||
DbValueSetV2::JwsKeyEs256(set) => set.len(),
|
||||
DbValueSetV2::JwsKeyRs256(set) => set.len(),
|
||||
}
|
||||
|
|
|
@ -1171,6 +1171,37 @@ pub const JSON_SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION: &str = r#"{
|
|||
}
|
||||
}"#;
|
||||
|
||||
pub const JSON_SCHEMA_ATTR_OAUTH2_SESSION: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"system",
|
||||
"attributetype"
|
||||
],
|
||||
"description": [
|
||||
"A session entry to an active oauth2 session, bound to a parent user auth token"
|
||||
],
|
||||
"index": [
|
||||
"EQUALITY"
|
||||
],
|
||||
"unique": [
|
||||
"false"
|
||||
],
|
||||
"multivalue": [
|
||||
"true"
|
||||
],
|
||||
"attributename": [
|
||||
"oauth2_session"
|
||||
],
|
||||
"syntax": [
|
||||
"OAUTH2SESSION"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000117"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
pub const JSON_SCHEMA_ATTR_SYNC_TOKEN_SESSION: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
|
@ -1371,7 +1402,8 @@ pub const JSON_SCHEMA_CLASS_ACCOUNT: &str = r#"
|
|||
"account_valid_from",
|
||||
"mail",
|
||||
"oauth2_consent_scope_map",
|
||||
"user_auth_token_session"
|
||||
"user_auth_token_session",
|
||||
"oauth2_session"
|
||||
],
|
||||
"systemmust": [
|
||||
"displayname",
|
||||
|
|
|
@ -201,6 +201,7 @@ pub const _UUID_SCHEMA_CLASS_SYNC_ACCOUNT: Uuid = uuid!("00000000-0000-0000-0000
|
|||
pub const _UUID_SCHEMA_ATTR_SYNC_TOKEN_SESSION: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-ffff00000115");
|
||||
pub const _UUID_SCHEMA_ATTR_SYNC_COOKIE: Uuid = uuid!("00000000-0000-0000-0000-ffff00000116");
|
||||
pub const _UUID_SCHEMA_ATTR_OAUTH2_SESSION: Uuid = uuid!("00000000-0000-0000-0000-ffff00000117");
|
||||
|
||||
// System and domain infos
|
||||
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
|
||||
|
|
|
@ -52,7 +52,9 @@ use crate::prelude::*;
|
|||
use crate::repl::cid::Cid;
|
||||
use crate::repl::entry::EntryChangelog;
|
||||
use crate::schema::{SchemaAttribute, SchemaClass, SchemaTransaction};
|
||||
use crate::value::{IndexType, IntentTokenState, PartialValue, Session, SyntaxType, Value};
|
||||
use crate::value::{
|
||||
IndexType, IntentTokenState, Oauth2Session, PartialValue, Session, SyntaxType, Value,
|
||||
};
|
||||
use crate::valueset::{self, ValueSet};
|
||||
|
||||
// use std::convert::TryFrom;
|
||||
|
@ -1863,6 +1865,16 @@ impl<VALID, STATE> Entry<VALID, STATE> {
|
|||
self.attrs.get(attr).and_then(|vs| vs.as_session_map())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get_ava_as_oauth2session_map(
|
||||
&self,
|
||||
attr: &str,
|
||||
) -> Option<&std::collections::BTreeMap<Uuid, Oauth2Session>> {
|
||||
self.attrs
|
||||
.get(attr)
|
||||
.and_then(|vs| vs.as_oauth2session_map())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
/// If possible, return an iterator over the set of values transformed into a `&str`.
|
||||
pub fn get_ava_iter_iname(&self, attr: &str) -> Option<impl Iterator<Item = &str>> {
|
||||
|
|
|
@ -13,6 +13,7 @@ pub enum DelayedAction {
|
|||
BackupCodeRemoval(BackupCodeRemoval),
|
||||
Oauth2ConsentGrant(Oauth2ConsentGrant),
|
||||
AuthSessionRecord(AuthSessionRecord),
|
||||
Oauth2SessionRecord(Oauth2SessionRecord),
|
||||
}
|
||||
|
||||
pub struct PasswordUpgrade {
|
||||
|
@ -70,3 +71,14 @@ pub struct AuthSessionRecord {
|
|||
pub issued_by: IdentityId,
|
||||
pub scope: AccessScope,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Oauth2SessionRecord {
|
||||
pub target_uuid: Uuid,
|
||||
pub parent_session_id: Uuid,
|
||||
pub session_id: Uuid,
|
||||
pub expiry: Option<OffsetDateTime>,
|
||||
pub issued_at: OffsetDateTime,
|
||||
// Which rs is this related to?
|
||||
pub rs_uuid: Uuid,
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ use hashbrown::HashMap;
|
|||
pub use kanidm_proto::oauth2::{
|
||||
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
|
||||
AccessTokenResponse, AuthorisationRequest, CodeChallengeMethod, ErrorResponse,
|
||||
OidcDiscoveryResponse,
|
||||
OidcDiscoveryResponse, TokenRevokeRequest,
|
||||
};
|
||||
use kanidm_proto::oauth2::{
|
||||
ClaimType, DisplayValue, GrantType, IdTokenSignAlg, ResponseMode, ResponseType, SubjectType,
|
||||
|
@ -34,8 +34,10 @@ use tracing::trace;
|
|||
use url::{Origin, Url};
|
||||
|
||||
use crate::identity::IdentityId;
|
||||
use crate::idm::delayed::{DelayedAction, Oauth2ConsentGrant};
|
||||
use crate::idm::server::{IdmServerProxyReadTransaction, IdmServerTransaction};
|
||||
use crate::idm::delayed::{DelayedAction, Oauth2ConsentGrant, Oauth2SessionRecord};
|
||||
use crate::idm::server::{
|
||||
IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction, IdmServerTransaction,
|
||||
};
|
||||
use crate::prelude::*;
|
||||
use crate::value::OAUTHSCOPE_RE;
|
||||
|
||||
|
@ -57,6 +59,8 @@ pub enum Oauth2Error {
|
|||
// from https://datatracker.ietf.org/doc/html/rfc6750
|
||||
InvalidToken,
|
||||
InsufficientScope,
|
||||
// from https://datatracker.ietf.org/doc/html/rfc7009#section-2.2.1
|
||||
UnsupportedTokenType,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Oauth2Error {
|
||||
|
@ -74,6 +78,7 @@ impl std::fmt::Display for Oauth2Error {
|
|||
Oauth2Error::TemporarilyUnavailable => "temporarily_unavailable",
|
||||
Oauth2Error::InvalidToken => "invalid_token",
|
||||
Oauth2Error::InsufficientScope => "insufficient_scope",
|
||||
Oauth2Error::UnsupportedTokenType => "unsupported_token_type",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -116,31 +121,38 @@ struct TokenExchangeCode {
|
|||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
enum Oauth2TokenType {
|
||||
Access(Oauth2AccessToken),
|
||||
Refresh,
|
||||
Access {
|
||||
scopes: Vec<String>,
|
||||
parent_session_id: Uuid,
|
||||
session_id: Uuid,
|
||||
auth_type: AuthType,
|
||||
expiry: time::OffsetDateTime,
|
||||
uuid: Uuid,
|
||||
iat: i64,
|
||||
nbf: i64,
|
||||
auth_time: Option<i64>,
|
||||
},
|
||||
Refresh {
|
||||
parent_session_id: Uuid,
|
||||
session_id: Uuid,
|
||||
expiry: time::OffsetDateTime,
|
||||
uuid: Uuid,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for Oauth2TokenType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Oauth2TokenType::Access(_) => write!(f, "access_token"),
|
||||
Oauth2TokenType::Refresh => write!(f, "refresh_token"),
|
||||
Oauth2TokenType::Access { session_id, .. } => {
|
||||
write!(f, "access_token ({}) ", session_id)
|
||||
}
|
||||
Oauth2TokenType::Refresh { session_id, .. } => {
|
||||
write!(f, "refresh_token ({}) ", session_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct Oauth2AccessToken {
|
||||
pub scopes: Vec<String>,
|
||||
pub session_id: Uuid,
|
||||
pub auth_type: AuthType,
|
||||
pub expiry: time::OffsetDateTime,
|
||||
pub uuid: Uuid,
|
||||
pub iat: i64,
|
||||
pub nbf: i64,
|
||||
pub auth_time: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AuthoriseResponse {
|
||||
ConsentRequested {
|
||||
|
@ -434,6 +446,92 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||
pub fn oauth2_token_revoke(
|
||||
&mut self,
|
||||
client_authz: &str,
|
||||
revoke_req: &TokenRevokeRequest,
|
||||
ct: Duration,
|
||||
) -> Result<(), Oauth2Error> {
|
||||
let (client_id, secret) = parse_basic_authz(client_authz)?;
|
||||
|
||||
// Get the o2rs for the handle.
|
||||
let o2rs = self.oauth2rs.inner.rs_set.get(&client_id).ok_or_else(|| {
|
||||
admin_warn!("Invalid oauth2 client_id");
|
||||
Oauth2Error::AuthenticationRequired
|
||||
})?;
|
||||
|
||||
// check the secret.
|
||||
if o2rs.authz_secret != secret {
|
||||
security_info!("Invalid oauth2 client_id secret");
|
||||
return Err(Oauth2Error::AuthenticationRequired);
|
||||
}
|
||||
// We are authenticated! Yay! Now we can actually check things ...
|
||||
|
||||
// Can we deserialise the token?
|
||||
let token: Oauth2TokenType = o2rs
|
||||
.token_fernet
|
||||
.decrypt(&revoke_req.token)
|
||||
.map_err(|_| {
|
||||
admin_error!("Failed to decrypt token introspection request");
|
||||
Oauth2Error::InvalidRequest
|
||||
})
|
||||
.and_then(|data| {
|
||||
serde_json::from_slice(&data).map_err(|e| {
|
||||
admin_error!("Failed to deserialise token - {:?}", e);
|
||||
Oauth2Error::InvalidRequest
|
||||
})
|
||||
})?;
|
||||
|
||||
// From these tokens, what we need is the identifiers that *might* exist,
|
||||
// such that we can remove them.
|
||||
match token {
|
||||
Oauth2TokenType::Access {
|
||||
session_id,
|
||||
expiry,
|
||||
uuid,
|
||||
..
|
||||
}
|
||||
| Oauth2TokenType::Refresh {
|
||||
session_id,
|
||||
expiry,
|
||||
uuid,
|
||||
..
|
||||
} => {
|
||||
// Only submit a revocation if the token is not yet expired.
|
||||
let odt_ct = OffsetDateTime::unix_epoch() + ct;
|
||||
if expiry <= odt_ct {
|
||||
security_info!(?uuid, "access token has expired, returning inactive");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Consider replication. We have servers A and B. A issues our oauth2
|
||||
// token to the client. The resource server then issues the revoke request
|
||||
// to B. In this case A has not yet replicated the session to B, but we
|
||||
// still need to ensure the revoke is respected. As a result, we don't
|
||||
// actually consult if the session is present on the account, we simply
|
||||
// submit the Modify::Remove. This way it's inserted into the entry changelog
|
||||
// and when replication converges the session is actually removed.
|
||||
|
||||
let modlist = ModifyList::new_list(vec![Modify::Removed(
|
||||
AttrString::from("oauth2_session"),
|
||||
PartialValue::Refer(session_id),
|
||||
)]);
|
||||
|
||||
self.qs_write
|
||||
.internal_modify(
|
||||
&filter!(f_eq("uuid", PartialValue::new_uuid(uuid))),
|
||||
&modlist,
|
||||
)
|
||||
.map_err(|e| {
|
||||
admin_error!("Failed to modify - revoke oauth2 session {:?}", e);
|
||||
Oauth2Error::ServerError(e)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Oauth2ResourceServersReadTransaction {
|
||||
pub fn check_oauth2_authorisation(
|
||||
&self,
|
||||
|
@ -853,15 +951,8 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
client_authz: Option<&str>,
|
||||
token_req: &AccessTokenRequest,
|
||||
ct: Duration,
|
||||
async_tx: &Sender<DelayedAction>,
|
||||
) -> Result<AccessTokenResponse, Oauth2Error> {
|
||||
// TODO: add refresh token grant type.
|
||||
// If it's a refresh token grant, are the consent permissions the same?
|
||||
|
||||
if token_req.grant_type != "authorization_code" {
|
||||
admin_warn!("Invalid oauth2 grant_type (should be 'authorization_code')");
|
||||
return Err(Oauth2Error::InvalidRequest);
|
||||
}
|
||||
|
||||
let (client_id, secret) = if let Some(client_authz) = client_authz {
|
||||
parse_basic_authz(client_authz)?
|
||||
} else {
|
||||
|
@ -887,8 +978,26 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
security_info!("Invalid oauth2 client_id secret");
|
||||
return Err(Oauth2Error::AuthenticationRequired);
|
||||
}
|
||||
|
||||
// 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?
|
||||
if token_req.grant_type == "authorization_code" {
|
||||
self.check_oauth2_token_exchange_authorization_code(o2rs, token_req, ct, async_tx)
|
||||
} else {
|
||||
admin_warn!("Invalid oauth2 grant_type (should be 'authorization_code')");
|
||||
return Err(Oauth2Error::InvalidRequest);
|
||||
}
|
||||
}
|
||||
|
||||
fn check_oauth2_token_exchange_authorization_code(
|
||||
&self,
|
||||
o2rs: &Oauth2RS,
|
||||
token_req: &AccessTokenRequest,
|
||||
ct: Duration,
|
||||
async_tx: &Sender<DelayedAction>,
|
||||
) -> Result<AccessTokenResponse, Oauth2Error> {
|
||||
// Check the token_req is within the valid time, and correctly signed for
|
||||
// this client.
|
||||
|
||||
|
@ -1023,7 +1132,7 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
let oidc = OidcToken {
|
||||
iss,
|
||||
sub: OidcSubject::U(code_xchg.uat.uuid),
|
||||
aud: client_id.clone(),
|
||||
aud: o2rs.name.clone(),
|
||||
iat,
|
||||
nbf: Some(iat),
|
||||
exp,
|
||||
|
@ -1032,7 +1141,7 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
at_hash: None,
|
||||
acr: None,
|
||||
amr,
|
||||
azp: Some(client_id.clone()),
|
||||
azp: Some(o2rs.name.clone()),
|
||||
jti: None,
|
||||
s_claims: OidcClaims {
|
||||
// Map from displayname
|
||||
|
@ -1061,17 +1170,22 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
None
|
||||
};
|
||||
|
||||
// TODO: Refresh tokens!
|
||||
let access_token_raw = Oauth2TokenType::Access(Oauth2AccessToken {
|
||||
let session_id = Uuid::new_v4();
|
||||
let parent_session_id = code_xchg.uat.session_id;
|
||||
|
||||
// We need to record this into the record? Delayed action?
|
||||
|
||||
let access_token_raw = Oauth2TokenType::Access {
|
||||
scopes: code_xchg.scopes,
|
||||
session_id: code_xchg.uat.session_id,
|
||||
parent_session_id,
|
||||
session_id,
|
||||
auth_type: code_xchg.uat.auth_type,
|
||||
expiry,
|
||||
uuid: code_xchg.uat.uuid,
|
||||
iat,
|
||||
nbf: iat,
|
||||
auth_time: None,
|
||||
});
|
||||
};
|
||||
|
||||
let access_token_data = serde_json::to_vec(&access_token_raw).map_err(|e| {
|
||||
admin_error!(err = ?e, "Unable to encode consent data");
|
||||
|
@ -1084,6 +1198,20 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
|
||||
let refresh_token = None;
|
||||
|
||||
async_tx
|
||||
.send(DelayedAction::Oauth2SessionRecord(Oauth2SessionRecord {
|
||||
target_uuid: code_xchg.uat.uuid,
|
||||
parent_session_id,
|
||||
session_id,
|
||||
expiry: Some(expiry),
|
||||
issued_at: odt_ct,
|
||||
rs_uuid: o2rs.uuid,
|
||||
}))
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Unable to submit oauth2 session record");
|
||||
Oauth2Error::ServerError(OperationError::InvalidState)
|
||||
})?;
|
||||
|
||||
Ok(AccessTokenResponse {
|
||||
access_token,
|
||||
token_type: "bearer".to_string(),
|
||||
|
@ -1125,40 +1253,53 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
})
|
||||
.and_then(|data| {
|
||||
serde_json::from_slice(&data).map_err(|e| {
|
||||
admin_error!("Failed to deserialise token exchange code - {:?}", e);
|
||||
admin_error!("Failed to deserialise token - {:?}", e);
|
||||
Oauth2Error::InvalidRequest
|
||||
})
|
||||
})?;
|
||||
|
||||
match token {
|
||||
Oauth2TokenType::Access(at) => {
|
||||
Oauth2TokenType::Access {
|
||||
scopes,
|
||||
parent_session_id,
|
||||
session_id,
|
||||
auth_type: _,
|
||||
expiry,
|
||||
uuid,
|
||||
iat,
|
||||
nbf,
|
||||
auth_time: _,
|
||||
} => {
|
||||
// Has this token expired?
|
||||
let odt_ct = OffsetDateTime::unix_epoch() + ct;
|
||||
if at.expiry <= odt_ct {
|
||||
security_info!(?at.uuid, "access token has expired, returning inactive");
|
||||
if expiry <= odt_ct {
|
||||
security_info!(?uuid, "access token has expired, returning inactive");
|
||||
return Ok(AccessTokenIntrospectResponse::inactive());
|
||||
}
|
||||
let exp = at.iat + ((at.expiry - odt_ct).whole_seconds() as i64);
|
||||
let exp = iat + ((expiry - odt_ct).whole_seconds() as i64);
|
||||
|
||||
// Is the user expired?
|
||||
// Is the user expired, or the oauth2 session invalid?
|
||||
let valid = idms
|
||||
.check_account_uuid_valid(&at.uuid, ct)
|
||||
.check_oauth2_account_uuid_valid(uuid, session_id, parent_session_id, iat, ct)
|
||||
.map_err(|_| admin_error!("Account is not valid"));
|
||||
|
||||
let account = match valid {
|
||||
Ok(Some(account)) => account,
|
||||
_ => {
|
||||
security_info!(?at.uuid, "access token has account not valid, returning inactive");
|
||||
security_info!(
|
||||
?uuid,
|
||||
"access token has account not valid, returning inactive"
|
||||
);
|
||||
return Ok(AccessTokenIntrospectResponse::inactive());
|
||||
}
|
||||
};
|
||||
|
||||
// ==== good to generate response ====
|
||||
|
||||
let scope = if at.scopes.is_empty() {
|
||||
let scope = if scopes.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(at.scopes.join(" "))
|
||||
Some(scopes.join(" "))
|
||||
};
|
||||
|
||||
let token_type = Some("access_token".to_string());
|
||||
|
@ -1169,15 +1310,15 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
username: Some(account.spn),
|
||||
token_type,
|
||||
exp: Some(exp),
|
||||
iat: Some(at.iat),
|
||||
nbf: Some(at.nbf),
|
||||
sub: Some(at.uuid.to_string()),
|
||||
iat: Some(iat),
|
||||
nbf: Some(nbf),
|
||||
sub: Some(uuid.to_string()),
|
||||
aud: Some(client_id),
|
||||
iss: None,
|
||||
jti: None,
|
||||
})
|
||||
}
|
||||
Oauth2TokenType::Refresh => Ok(AccessTokenIntrospectResponse::inactive()),
|
||||
Oauth2TokenType::Refresh { .. } => Ok(AccessTokenIntrospectResponse::inactive()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1210,29 +1351,42 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
})?;
|
||||
|
||||
match token {
|
||||
Oauth2TokenType::Access(at) => {
|
||||
Oauth2TokenType::Access {
|
||||
scopes,
|
||||
parent_session_id,
|
||||
session_id,
|
||||
auth_type,
|
||||
expiry,
|
||||
uuid,
|
||||
iat,
|
||||
nbf,
|
||||
auth_time: _,
|
||||
} => {
|
||||
// Has this token expired?
|
||||
let odt_ct = OffsetDateTime::unix_epoch() + ct;
|
||||
if at.expiry <= odt_ct {
|
||||
security_info!(?at.uuid, "access token has expired, returning inactive");
|
||||
if expiry <= odt_ct {
|
||||
security_info!(?uuid, "access token has expired, returning inactive");
|
||||
return Err(Oauth2Error::InvalidToken);
|
||||
}
|
||||
let exp = at.iat + ((at.expiry - odt_ct).whole_seconds() as i64);
|
||||
let exp = iat + ((expiry - odt_ct).whole_seconds() as i64);
|
||||
|
||||
// Is the user expired?
|
||||
// Is the user expired, or the oauth2 session invalid?
|
||||
let valid = idms
|
||||
.check_account_uuid_valid(&at.uuid, ct)
|
||||
.check_oauth2_account_uuid_valid(uuid, session_id, parent_session_id, iat, ct)
|
||||
.map_err(|_| admin_error!("Account is not valid"));
|
||||
|
||||
let account = match valid {
|
||||
Ok(Some(account)) => account,
|
||||
_ => {
|
||||
security_info!(?at.uuid, "access token has account not valid, returning inactive");
|
||||
security_info!(
|
||||
?uuid,
|
||||
"access token has account not valid, returning inactive"
|
||||
);
|
||||
return Err(Oauth2Error::InvalidToken);
|
||||
}
|
||||
};
|
||||
|
||||
let (email, email_verified) = if at.scopes.contains(&"email".to_string()) {
|
||||
let (email, email_verified) = if scopes.contains(&"email".to_string()) {
|
||||
if let Some(mp) = account.mail_primary {
|
||||
(Some(mp), Some(true))
|
||||
} else {
|
||||
|
@ -1242,7 +1396,7 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
(None, None)
|
||||
};
|
||||
|
||||
let amr = Some(vec![at.auth_type.to_string()]);
|
||||
let amr = Some(vec![auth_type.to_string()]);
|
||||
|
||||
let iss = o2rs.iss.clone();
|
||||
|
||||
|
@ -1250,10 +1404,10 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
|
||||
Ok(OidcToken {
|
||||
iss,
|
||||
sub: OidcSubject::U(at.uuid),
|
||||
sub: OidcSubject::U(uuid),
|
||||
aud: client_id.to_string(),
|
||||
iat: at.iat,
|
||||
nbf: Some(at.nbf),
|
||||
iat: iat,
|
||||
nbf: Some(nbf),
|
||||
exp,
|
||||
auth_time: None,
|
||||
nonce: None,
|
||||
|
@ -1267,7 +1421,7 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
name: Some(account.displayname.clone()),
|
||||
// Map from spn
|
||||
preferred_username: Some(account.spn),
|
||||
scopes: at.scopes,
|
||||
scopes: scopes,
|
||||
email,
|
||||
email_verified,
|
||||
..Default::default()
|
||||
|
@ -1276,7 +1430,7 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
})
|
||||
}
|
||||
// https://openid.net/specs/openid-connect-basic-1_0.html#UserInfoErrorResponse
|
||||
Oauth2TokenType::Refresh => Err(Oauth2Error::InvalidToken),
|
||||
Oauth2TokenType::Refresh { .. } => Err(Oauth2Error::InvalidToken),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1637,6 +1791,12 @@ mod tests {
|
|||
.check_oauth2_token_exchange(None, &token_req, ct)
|
||||
.expect("Failed to perform oauth2 token exchange");
|
||||
|
||||
// Assert that the session creation was submitted
|
||||
match idms_delayed.async_rx.blocking_recv() {
|
||||
Some(DelayedAction::Oauth2SessionRecord(_)) => {}
|
||||
_ => assert!(false),
|
||||
}
|
||||
|
||||
// 🎉 We got a token! In the future we can then check introspection from this point.
|
||||
assert!(token_response.token_type == "bearer");
|
||||
}
|
||||
|
@ -2109,6 +2269,12 @@ mod tests {
|
|||
.check_oauth2_token_exchange(client_authz.as_deref(), &token_req, ct)
|
||||
.expect("Unable to exchange for oauth2 token");
|
||||
|
||||
// Assert that the session creation was submitted
|
||||
match idms_delayed.async_rx.blocking_recv() {
|
||||
Some(DelayedAction::Oauth2SessionRecord(_)) => {}
|
||||
_ => assert!(false),
|
||||
}
|
||||
|
||||
// Okay, now we have the token, we can check it works with introspect.
|
||||
let intr_request = AccessTokenIntrospectRequest {
|
||||
token: oauth2_token.access_token.clone(),
|
||||
|
@ -2163,6 +2329,287 @@ mod tests {
|
|||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idm_oauth2_token_revoke() {
|
||||
run_idm_test!(
|
||||
|_qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
|
||||
// First, setup to get a token.
|
||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||
let (secret, uat, ident, _) = setup_oauth2_resource_server(idms, ct, true, false);
|
||||
let client_authz = Some(base64::encode(format!("test_resource_server:{}", secret)));
|
||||
|
||||
let idms_prox_read = task::block_on(idms.proxy_read());
|
||||
|
||||
// == Setup the authorisation request
|
||||
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||
let consent_request =
|
||||
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
|
||||
|
||||
let consent_token =
|
||||
if let AuthoriseResponse::ConsentRequested { consent_token, .. } =
|
||||
consent_request
|
||||
{
|
||||
consent_token
|
||||
} else {
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
// == Manually submit the consent token to the permit for the permit_success
|
||||
let permit_success = idms_prox_read
|
||||
.check_oauth2_authorise_permit(&ident, &uat, &consent_token, ct)
|
||||
.expect("Failed to perform oauth2 permit");
|
||||
|
||||
// Assert that the consent was submitted
|
||||
match idms_delayed.async_rx.blocking_recv() {
|
||||
Some(DelayedAction::Oauth2ConsentGrant(_)) => {}
|
||||
_ => assert!(false),
|
||||
}
|
||||
|
||||
let token_req = AccessTokenRequest {
|
||||
grant_type: "authorization_code".to_string(),
|
||||
code: permit_success.code.clone(),
|
||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
||||
client_id: None,
|
||||
client_secret: None,
|
||||
code_verifier,
|
||||
};
|
||||
let oauth2_token = idms_prox_read
|
||||
.check_oauth2_token_exchange(client_authz.as_deref(), &token_req, ct)
|
||||
.expect("Unable to exchange for oauth2 token");
|
||||
|
||||
drop(idms_prox_read);
|
||||
|
||||
// Assert that the session creation was submitted
|
||||
match idms_delayed.async_rx.blocking_recv() {
|
||||
Some(DelayedAction::Oauth2SessionRecord(osr)) => {
|
||||
// Process it to ensure the record exists.
|
||||
let mut idms_prox_write = task::block_on(idms.proxy_write(ct));
|
||||
|
||||
assert!(idms_prox_write
|
||||
.process_oauth2sessionrecord(&osr)
|
||||
.is_ok());
|
||||
|
||||
assert!(idms_prox_write.commit().is_ok());
|
||||
}
|
||||
_ => assert!(false),
|
||||
}
|
||||
|
||||
// Okay, now we have the token, we can check behaviours with the revoke interface.
|
||||
|
||||
// First, assert it is valid, similar to the introspect api.
|
||||
let idms_prox_read = task::block_on(idms.proxy_read());
|
||||
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);
|
||||
drop(idms_prox_read);
|
||||
|
||||
// First, the revoke needs basic auth. Provide incorrect auth, and we fail.
|
||||
let mut idms_prox_write = task::block_on(idms.proxy_write(ct));
|
||||
|
||||
let bad_client_authz = Some(base64::encode("test_resource_server:12345"));
|
||||
let revoke_request = TokenRevokeRequest {
|
||||
token: oauth2_token.access_token.clone(),
|
||||
token_type_hint: None,
|
||||
};
|
||||
let e = idms_prox_write
|
||||
.oauth2_token_revoke(bad_client_authz.as_deref().unwrap(), &revoke_request, ct)
|
||||
.unwrap_err();
|
||||
assert!(matches!(e, Oauth2Error::AuthenticationRequired));
|
||||
assert!(idms_prox_write.commit().is_ok());
|
||||
|
||||
// Now submit a non-existant/invalid token. Does not affect our tokens validity.
|
||||
let mut idms_prox_write = task::block_on(idms.proxy_write(ct));
|
||||
let revoke_request = TokenRevokeRequest {
|
||||
token: "this is an invalid token, nothing will happen!".to_string(),
|
||||
token_type_hint: None,
|
||||
};
|
||||
let e = idms_prox_write
|
||||
.oauth2_token_revoke(client_authz.as_deref().unwrap(), &revoke_request, ct)
|
||||
.unwrap_err();
|
||||
assert!(matches!(e, Oauth2Error::InvalidRequest));
|
||||
assert!(idms_prox_write.commit().is_ok());
|
||||
|
||||
// Check our token is still valid.
|
||||
let idms_prox_read = task::block_on(idms.proxy_read());
|
||||
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);
|
||||
|
||||
// Finally revoke it.
|
||||
let mut idms_prox_write = task::block_on(idms.proxy_write(ct));
|
||||
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());
|
||||
|
||||
// Check it is still valid - this is because we are still in the GRACE window.
|
||||
let idms_prox_read = task::block_on(idms.proxy_read());
|
||||
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);
|
||||
|
||||
// Check after the grace window, it will be invalid.
|
||||
let ct = ct + GRACE_WINDOW;
|
||||
|
||||
// Assert it is now invalid.
|
||||
let idms_prox_read = task::block_on(idms.proxy_read());
|
||||
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);
|
||||
|
||||
// A second invalidation of the token "does nothing".
|
||||
let mut idms_prox_write = task::block_on(idms.proxy_write(ct));
|
||||
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());
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idm_oauth2_session_cleanup_post_rs_delete() {
|
||||
run_idm_test!(
|
||||
|_qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
|
||||
// First, setup to get a token.
|
||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||
let (secret, uat, ident, _) = setup_oauth2_resource_server(idms, ct, true, false);
|
||||
let client_authz = Some(base64::encode(format!("test_resource_server:{}", secret)));
|
||||
|
||||
let idms_prox_read = task::block_on(idms.proxy_read());
|
||||
|
||||
// == Setup the authorisation request
|
||||
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||
let consent_request =
|
||||
good_authorisation_request!(idms_prox_read, &ident, &uat, ct, code_challenge);
|
||||
|
||||
let consent_token =
|
||||
if let AuthoriseResponse::ConsentRequested { consent_token, .. } =
|
||||
consent_request
|
||||
{
|
||||
consent_token
|
||||
} else {
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
// == Manually submit the consent token to the permit for the permit_success
|
||||
let permit_success = idms_prox_read
|
||||
.check_oauth2_authorise_permit(&ident, &uat, &consent_token, ct)
|
||||
.expect("Failed to perform oauth2 permit");
|
||||
|
||||
// Assert that the consent was submitted
|
||||
match idms_delayed.async_rx.blocking_recv() {
|
||||
Some(DelayedAction::Oauth2ConsentGrant(_)) => {}
|
||||
_ => assert!(false),
|
||||
}
|
||||
|
||||
let token_req = AccessTokenRequest {
|
||||
grant_type: "authorization_code".to_string(),
|
||||
code: permit_success.code.clone(),
|
||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
||||
client_id: None,
|
||||
client_secret: None,
|
||||
code_verifier,
|
||||
};
|
||||
let _oauth2_token = idms_prox_read
|
||||
.check_oauth2_token_exchange(client_authz.as_deref(), &token_req, ct)
|
||||
.expect("Unable to exchange for oauth2 token");
|
||||
|
||||
drop(idms_prox_read);
|
||||
|
||||
// Process it to ensure the record exists.
|
||||
let mut idms_prox_write = task::block_on(idms.proxy_write(ct));
|
||||
|
||||
// Assert that the session creation was submitted
|
||||
let session_id = match idms_delayed.async_rx.blocking_recv() {
|
||||
Some(DelayedAction::Oauth2SessionRecord(osr)) => {
|
||||
assert!(idms_prox_write
|
||||
.process_oauth2sessionrecord(&osr)
|
||||
.is_ok());
|
||||
osr.session_id
|
||||
}
|
||||
_ => {
|
||||
unreachable!();
|
||||
}
|
||||
};
|
||||
|
||||
// Check it is now there
|
||||
let entry = idms_prox_write
|
||||
.qs_write
|
||||
.internal_search_uuid(&UUID_ADMIN)
|
||||
.expect("failed");
|
||||
let valid = entry
|
||||
.get_ava_as_oauth2session_map("oauth2_session")
|
||||
.map(|map| map.get(&session_id).is_some())
|
||||
.unwrap_or(false);
|
||||
assert!(valid);
|
||||
|
||||
// Delete the resource server.
|
||||
|
||||
let de = unsafe {
|
||||
DeleteEvent::new_internal_invalid(filter!(f_eq(
|
||||
"oauth2_rs_name",
|
||||
PartialValue::new_iname("test_resource_server")
|
||||
)))
|
||||
};
|
||||
|
||||
assert!(idms_prox_write.qs_write.delete(&de).is_ok());
|
||||
|
||||
// Assert the session is gone. This is cleaned up as an artifact of the referential
|
||||
// integrity plugin.
|
||||
let entry = idms_prox_write
|
||||
.qs_write
|
||||
.internal_search_uuid(&UUID_ADMIN)
|
||||
.expect("failed");
|
||||
let valid = entry
|
||||
.get_ava_as_oauth2session_map("oauth2_session")
|
||||
.map(|map| map.get(&session_id).is_some())
|
||||
.unwrap_or(false);
|
||||
assert!(!valid);
|
||||
|
||||
assert!(idms_prox_write.commit().is_ok());
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idm_oauth2_authorisation_reject() {
|
||||
run_idm_test!(|_qs: &QueryServer,
|
||||
|
@ -2439,6 +2886,12 @@ mod tests {
|
|||
.check_oauth2_token_exchange(client_authz.as_deref(), &token_req, ct)
|
||||
.expect("Failed to perform oauth2 token exchange");
|
||||
|
||||
// Assert that the session creation was submitted
|
||||
match idms_delayed.async_rx.blocking_recv() {
|
||||
Some(DelayedAction::Oauth2SessionRecord(_)) => {}
|
||||
_ => assert!(false),
|
||||
}
|
||||
|
||||
// 🎉 We got a token!
|
||||
assert!(token_response.token_type == "bearer");
|
||||
|
||||
|
@ -2625,6 +3078,12 @@ mod tests {
|
|||
.check_oauth2_token_exchange(None, &token_req, ct)
|
||||
.expect("Failed to perform oauth2 token exchange");
|
||||
|
||||
// Assert that the session creation was submitted
|
||||
match idms_delayed.async_rx.blocking_recv() {
|
||||
Some(DelayedAction::Oauth2SessionRecord(_)) => {}
|
||||
_ => assert!(false),
|
||||
}
|
||||
|
||||
// 🎉 We got a token!
|
||||
assert!(token_response.token_type == "bearer");
|
||||
let id_token = token_response.id_token.expect("No id_token in response!");
|
||||
|
|
|
@ -16,7 +16,6 @@ use kanidm_proto::v1::{
|
|||
UnixGroupToken, UnixUserToken, UserAuthToken,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::sync::mpsc::{
|
||||
unbounded_channel as unbounded, UnboundedReceiver as Receiver, UnboundedSender as Sender,
|
||||
};
|
||||
|
@ -33,8 +32,8 @@ use crate::idm::account::Account;
|
|||
use crate::idm::authsession::AuthSession;
|
||||
use crate::idm::credupdatesession::CredentialUpdateSessionMutex;
|
||||
use crate::idm::delayed::{
|
||||
AuthSessionRecord, BackupCodeRemoval, DelayedAction, Oauth2ConsentGrant, PasswordUpgrade,
|
||||
UnixPasswordUpgrade, WebauthnCounterIncrement,
|
||||
AuthSessionRecord, BackupCodeRemoval, DelayedAction, Oauth2ConsentGrant, Oauth2SessionRecord,
|
||||
PasswordUpgrade, UnixPasswordUpgrade, WebauthnCounterIncrement,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use crate::idm::event::PasswordChangeEvent;
|
||||
|
@ -58,7 +57,7 @@ use crate::idm::AuthState;
|
|||
use crate::ldap::{LdapBoundToken, LdapSession};
|
||||
use crate::prelude::*;
|
||||
use crate::utils::{password_from_random, readable_password_from_random, uuid_from_duration, Sid};
|
||||
use crate::value::Session;
|
||||
use crate::value::{Oauth2Session, Session};
|
||||
|
||||
type AuthSessionMutex = Arc<Mutex<AuthSession>>;
|
||||
type CredSoftLockMutex = Arc<Mutex<CredSoftLock>>;
|
||||
|
@ -135,7 +134,7 @@ pub struct IdmServerProxyWriteTransaction<'a> {
|
|||
uat_jwt_signer: CowCellWriteTxn<'a, JwsSigner>,
|
||||
uat_jwt_validator: CowCellWriteTxn<'a, JwsValidator>,
|
||||
pub(crate) token_enc_key: CowCellWriteTxn<'a, Fernet>,
|
||||
oauth2rs: Oauth2ResourceServersWriteTransaction<'a>,
|
||||
pub(crate) oauth2rs: Oauth2ResourceServersWriteTransaction<'a>,
|
||||
}
|
||||
|
||||
pub struct IdmServerDelayed {
|
||||
|
@ -574,25 +573,54 @@ pub trait IdmServerTransaction<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn check_account_uuid_valid(
|
||||
fn check_oauth2_account_uuid_valid(
|
||||
&self,
|
||||
uuid: &Uuid,
|
||||
uuid: Uuid,
|
||||
session_id: Uuid,
|
||||
parent_session_id: Uuid,
|
||||
iat: i64,
|
||||
ct: Duration,
|
||||
) -> Result<Option<Account>, OperationError> {
|
||||
let entry = self.get_qs_txn().internal_search_uuid(uuid).map_err(|e| {
|
||||
admin_error!(?e, "check_account_uuid_valid failed");
|
||||
let entry = self.get_qs_txn().internal_search_uuid(&uuid).map_err(|e| {
|
||||
admin_error!(?e, "check_oauth2_account_uuid_valid failed");
|
||||
e
|
||||
})?;
|
||||
|
||||
if Account::check_within_valid_time(
|
||||
let within_valid_window = Account::check_within_valid_time(
|
||||
ct,
|
||||
entry.get_ava_single_datetime("account_valid_from").as_ref(),
|
||||
entry.get_ava_single_datetime("account_expire").as_ref(),
|
||||
) {
|
||||
Account::try_from_entry_no_groups(entry.as_ref()).map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
);
|
||||
|
||||
if !within_valid_window {
|
||||
security_info!("Account has expired or is not yet valid, not allowing to proceed");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if ct >= Duration::from_secs(iat as u64) + GRACE_WINDOW {
|
||||
// We are past the grace window. Enforce session presence.
|
||||
// We enforce both sessions are present in case of inconsistency
|
||||
// that may occur with replication.
|
||||
let oauth2_session_valid = entry
|
||||
.get_ava_as_oauth2session_map("oauth2_session")
|
||||
.map(|map| map.get(&session_id).is_some())
|
||||
.unwrap_or(false);
|
||||
let uat_session_valid = entry
|
||||
.get_ava_as_session_map("user_auth_token_session")
|
||||
.map(|map| map.get(&parent_session_id).is_some())
|
||||
.unwrap_or(false);
|
||||
|
||||
if oauth2_session_valid && uat_session_valid {
|
||||
security_info!("A valid session value exists for this token");
|
||||
} else {
|
||||
security_info!(%uat_session_valid, %oauth2_session_valid, "The token grace window has passed and no sessions exist. Assuming invalid.");
|
||||
return Ok(None);
|
||||
}
|
||||
} else {
|
||||
security_info!("The token grace window is in effect. Assuming valid.");
|
||||
};
|
||||
|
||||
Account::try_from_entry_no_groups(entry.as_ref()).map(Some)
|
||||
}
|
||||
|
||||
/// For any event/operation to proceed, we need to attach an identity to the
|
||||
|
@ -1528,7 +1556,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
|||
ct: Duration,
|
||||
) -> Result<AccessTokenResponse, Oauth2Error> {
|
||||
self.oauth2rs
|
||||
.check_oauth2_token_exchange(client_authz, token_req, ct)
|
||||
.check_oauth2_token_exchange(client_authz, token_req, ct, &self.async_tx)
|
||||
}
|
||||
|
||||
pub fn check_oauth2_token_introspect(
|
||||
|
@ -2103,13 +2131,9 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
pub(crate) fn process_authsessionrecord(
|
||||
&mut self,
|
||||
asr: &AuthSessionRecord,
|
||||
ct: Duration,
|
||||
) -> Result<(), OperationError> {
|
||||
// We have to get the entry so we can work out if we need to expire any of it's sessions.
|
||||
|
||||
let entry = self.qs_write.internal_search_uuid(&asr.target_uuid)?;
|
||||
let sessions = entry.get_ava_as_session_map("user_auth_token_session");
|
||||
|
||||
let session = Value::Session(
|
||||
asr.session_id,
|
||||
Session {
|
||||
|
@ -2128,33 +2152,8 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
|
||||
info!(session_id = %asr.session_id, "Persisting auth session");
|
||||
|
||||
let offset_ct = OffsetDateTime::unix_epoch() + ct;
|
||||
|
||||
let mlist: Vec<_> = sessions
|
||||
.iter()
|
||||
.flat_map(|item| item.iter())
|
||||
.filter_map(|(k, v)| {
|
||||
// We only check if an expiry is present
|
||||
v.expiry.and_then(|exp| {
|
||||
if exp <= offset_ct {
|
||||
info!(session_id = %k, "Removing expired auth session");
|
||||
Some(Modify::Removed(
|
||||
AttrString::from("user_auth_token_session"),
|
||||
PartialValue::Refer(*k),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.chain(std::iter::once(Modify::Present(
|
||||
AttrString::from("user_auth_token_session"),
|
||||
session,
|
||||
)))
|
||||
.collect();
|
||||
|
||||
// modify the account to put the session onto it.
|
||||
let modlist = ModifyList::new_list(mlist);
|
||||
let modlist = ModifyList::new_append("user_auth_token_session", session);
|
||||
|
||||
self.qs_write
|
||||
.internal_modify(
|
||||
|
@ -2189,10 +2188,44 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
)
|
||||
}
|
||||
|
||||
pub(crate) fn process_oauth2sessionrecord(
|
||||
&mut self,
|
||||
osr: &Oauth2SessionRecord,
|
||||
) -> Result<(), OperationError> {
|
||||
let session = Value::Oauth2Session(
|
||||
osr.session_id,
|
||||
Oauth2Session {
|
||||
parent: osr.parent_session_id,
|
||||
expiry: osr.expiry,
|
||||
issued_at: osr.issued_at,
|
||||
rs_uuid: osr.rs_uuid,
|
||||
},
|
||||
);
|
||||
|
||||
info!(session_id = %osr.session_id, "Persisting auth session");
|
||||
|
||||
// modify the account to put the session onto it.
|
||||
let modlist = ModifyList::new_append(
|
||||
"oauth2_session",
|
||||
session,
|
||||
);
|
||||
|
||||
self.qs_write
|
||||
.internal_modify(
|
||||
&filter!(f_eq("uuid", PartialValue::new_uuid(osr.target_uuid))),
|
||||
&modlist,
|
||||
)
|
||||
.map_err(|e| {
|
||||
admin_error!("Failed to persist user auth token {:?}", e);
|
||||
e
|
||||
})
|
||||
// Done!
|
||||
}
|
||||
|
||||
pub fn process_delayedaction(
|
||||
&mut self,
|
||||
da: DelayedAction,
|
||||
ct: Duration,
|
||||
_ct: Duration,
|
||||
) -> Result<(), OperationError> {
|
||||
match da {
|
||||
DelayedAction::PwUpgrade(pwu) => self.process_pwupgrade(&pwu),
|
||||
|
@ -2200,7 +2233,8 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
DelayedAction::WebauthnCounterIncrement(wci) => self.process_webauthncounterinc(&wci),
|
||||
DelayedAction::BackupCodeRemoval(bcr) => self.process_backupcoderemoval(&bcr),
|
||||
DelayedAction::Oauth2ConsentGrant(o2cg) => self.process_oauth2consentgrant(&o2cg),
|
||||
DelayedAction::AuthSessionRecord(asr) => self.process_authsessionrecord(&asr, ct),
|
||||
DelayedAction::AuthSessionRecord(asr) => self.process_authsessionrecord(&asr),
|
||||
DelayedAction::Oauth2SessionRecord(osr) => self.process_oauth2sessionrecord(&osr),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2307,7 +2341,6 @@ mod tests {
|
|||
const TEST_PASSWORD: &'static str = "ntaoeuntnaoeuhraohuercahu😍";
|
||||
const TEST_PASSWORD_INC: &'static str = "ntaoentu nkrcgaeunhibwmwmqj;k wqjbkx ";
|
||||
const TEST_CURRENT_TIME: u64 = 6000;
|
||||
const TEST_CURRENT_EXPIRE: u64 = TEST_CURRENT_TIME + AUTH_SESSION_TIMEOUT + 1;
|
||||
|
||||
#[test]
|
||||
fn test_idm_anonymous_auth() {
|
||||
|
@ -2742,36 +2775,6 @@ mod tests {
|
|||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idm_session_expire() {
|
||||
run_idm_test!(
|
||||
|qs: &QueryServer, idms: &IdmServer, _idms_delayed: &IdmServerDelayed| {
|
||||
task::block_on(init_admin_w_password(qs, TEST_PASSWORD))
|
||||
.expect("Failed to setup admin account");
|
||||
let sid = init_admin_authsession_sid(
|
||||
idms,
|
||||
Duration::from_secs(TEST_CURRENT_TIME),
|
||||
"admin",
|
||||
);
|
||||
let mut idms_auth = idms.auth();
|
||||
assert!(idms_auth.is_sessionid_present(&sid));
|
||||
// Expire like we are currently "now". Should not affect our session.
|
||||
task::block_on(
|
||||
idms_auth.expire_auth_sessions(Duration::from_secs(TEST_CURRENT_TIME)),
|
||||
);
|
||||
assert!(idms_auth.is_sessionid_present(&sid));
|
||||
// Expire as though we are in the future.
|
||||
task::block_on(
|
||||
idms_auth.expire_auth_sessions(Duration::from_secs(TEST_CURRENT_EXPIRE)),
|
||||
);
|
||||
assert!(!idms_auth.is_sessionid_present(&sid));
|
||||
assert!(idms_auth.commit().is_ok());
|
||||
let idms_auth = idms.auth();
|
||||
assert!(!idms_auth.is_sessionid_present(&sid));
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idm_regenerate_radius_secret() {
|
||||
run_idm_test!(
|
||||
|
@ -3728,7 +3731,8 @@ mod tests {
|
|||
idms: &IdmServer,
|
||||
_idms_delayed: &mut IdmServerDelayed| {
|
||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||
let expiry = ct + Duration::from_secs(AUTH_SESSION_EXPIRY + 1);
|
||||
let expiry_a = ct + Duration::from_secs(AUTH_SESSION_EXPIRY + 1);
|
||||
let expiry_b = ct + Duration::from_secs((AUTH_SESSION_EXPIRY + 1) * 2);
|
||||
|
||||
let session_a = Uuid::new_v4();
|
||||
let session_b = Uuid::new_v4();
|
||||
|
@ -3747,7 +3751,7 @@ mod tests {
|
|||
target_uuid: UUID_ADMIN,
|
||||
session_id: session_a,
|
||||
label: "Test Session A".to_string(),
|
||||
expiry: Some(OffsetDateTime::unix_epoch() + expiry),
|
||||
expiry: Some(OffsetDateTime::unix_epoch() + expiry_a),
|
||||
issued_at: OffsetDateTime::unix_epoch() + ct,
|
||||
issued_by: IdentityId::User(UUID_ADMIN),
|
||||
scope: AccessScope::IdentityOnly,
|
||||
|
@ -3781,13 +3785,13 @@ mod tests {
|
|||
target_uuid: UUID_ADMIN,
|
||||
session_id: session_b,
|
||||
label: "Test Session B".to_string(),
|
||||
expiry: Some(OffsetDateTime::unix_epoch() + expiry),
|
||||
expiry: Some(OffsetDateTime::unix_epoch() + expiry_b),
|
||||
issued_at: OffsetDateTime::unix_epoch() + ct,
|
||||
issued_by: IdentityId::User(UUID_ADMIN),
|
||||
scope: AccessScope::IdentityOnly,
|
||||
});
|
||||
// Persist it.
|
||||
let r = task::block_on(idms.delayed_action(expiry, da));
|
||||
let r = task::block_on(idms.delayed_action(expiry_a, da));
|
||||
assert!(Ok(true) == r);
|
||||
|
||||
let idms_prox_read = task::block_on(idms.proxy_read());
|
||||
|
@ -3836,7 +3840,7 @@ mod tests {
|
|||
// Process the session info.
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
|
||||
let r = task::block_on(idms.delayed_action(duration_from_epoch_now(), da));
|
||||
let r = task::block_on(idms.delayed_action(ct, da));
|
||||
assert!(Ok(true) == r);
|
||||
|
||||
let uat_unverified =
|
||||
|
|
|
@ -21,6 +21,7 @@ mod memberof;
|
|||
mod password_import;
|
||||
mod protected;
|
||||
mod refint;
|
||||
mod session;
|
||||
mod spn;
|
||||
|
||||
trait Plugin {
|
||||
|
@ -165,6 +166,7 @@ impl Plugins {
|
|||
.and_then(|_| gidnumber::GidNumber::pre_modify(qs, cand, me))
|
||||
.and_then(|_| domain::Domain::pre_modify(qs, cand, me))
|
||||
.and_then(|_| spn::Spn::pre_modify(qs, cand, me))
|
||||
.and_then(|_| session::SessionConsistency::pre_modify(qs, cand, me))
|
||||
// attr unique should always be last
|
||||
.and_then(|_| attrunique::AttrUnique::pre_modify(qs, cand, me))
|
||||
}
|
||||
|
|
|
@ -146,7 +146,7 @@ impl Plugin for ReferentialIntegrity {
|
|||
})
|
||||
.map(|v| {
|
||||
v.to_ref_uuid()
|
||||
.map(|uuid| PartialValue::new_uuid(*uuid))
|
||||
.map(|uuid| PartialValue::new_uuid(uuid))
|
||||
.ok_or_else(|| {
|
||||
admin_error!(?v, "reference value could not convert to reference uuid.");
|
||||
admin_error!("If you are sure the name/uuid/spn exist, and that this is in error, you should run a verify task.");
|
||||
|
@ -267,7 +267,11 @@ impl Plugin for ReferentialIntegrity {
|
|||
mod tests {
|
||||
use kanidm_proto::v1::PluginError;
|
||||
|
||||
use crate::event::CreateEvent;
|
||||
use crate::prelude::*;
|
||||
use crate::value::{Oauth2Session, Session};
|
||||
use time::OffsetDateTime;
|
||||
use uuid::uuid;
|
||||
|
||||
// The create references a uuid that doesn't exist - reject
|
||||
#[test]
|
||||
|
@ -775,4 +779,121 @@ mod tests {
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[qs_test]
|
||||
async fn test_delete_oauth2_rs_remove_sessions(server: &QueryServer) {
|
||||
let curtime = duration_from_epoch_now();
|
||||
let curtime_odt = OffsetDateTime::unix_epoch() + curtime;
|
||||
|
||||
// Create a user
|
||||
let mut server_txn = server.write(curtime).await;
|
||||
|
||||
let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
|
||||
let rs_uuid = Uuid::new_v4();
|
||||
|
||||
let e1 = entry_init!(
|
||||
("class", Value::new_class("object")),
|
||||
("class", Value::new_class("person")),
|
||||
("class", Value::new_class("account")),
|
||||
("name", Value::new_iname("testperson1")),
|
||||
("uuid", Value::new_uuid(tuuid)),
|
||||
("description", Value::new_utf8s("testperson1")),
|
||||
("displayname", Value::new_utf8s("testperson1"))
|
||||
);
|
||||
|
||||
let e2 = entry_init!(
|
||||
("class", Value::new_class("object")),
|
||||
("class", Value::new_class("oauth2_resource_server")),
|
||||
("class", Value::new_class("oauth2_resource_server_basic")),
|
||||
("uuid", Value::new_uuid(rs_uuid)),
|
||||
("oauth2_rs_name", Value::new_iname("test_resource_server")),
|
||||
("displayname", Value::new_utf8s("test_resource_server")),
|
||||
(
|
||||
"oauth2_rs_origin",
|
||||
Value::new_url_s("https://demo.example.com").unwrap()
|
||||
),
|
||||
// System admins
|
||||
(
|
||||
"oauth2_rs_scope_map",
|
||||
Value::new_oauthscopemap(UUID_IDM_ALL_ACCOUNTS, btreeset!["openid".to_string()])
|
||||
.expect("invalid oauthscope")
|
||||
)
|
||||
);
|
||||
|
||||
let ce = CreateEvent::new_internal(vec![e1, e2]);
|
||||
assert!(server_txn.create(&ce).is_ok());
|
||||
|
||||
// Create a fake session and oauth2 session.
|
||||
|
||||
let session_id = Uuid::new_v4();
|
||||
let pv_session_id = PartialValue::new_refer(session_id);
|
||||
|
||||
let parent = Uuid::new_v4();
|
||||
let pv_parent_id = PartialValue::new_refer(parent);
|
||||
let issued_at = curtime_odt;
|
||||
let issued_by = IdentityId::User(tuuid);
|
||||
let scope = AccessScope::IdentityOnly;
|
||||
|
||||
// Mod the user
|
||||
let modlist = modlist!([
|
||||
Modify::Present(
|
||||
"oauth2_session".into(),
|
||||
Value::Oauth2Session(
|
||||
session_id,
|
||||
Oauth2Session {
|
||||
parent,
|
||||
// Note we set the exp to None so we are not removing based on exp
|
||||
expiry: None,
|
||||
issued_at,
|
||||
rs_uuid,
|
||||
},
|
||||
)
|
||||
),
|
||||
Modify::Present(
|
||||
"user_auth_token_session".into(),
|
||||
Value::Session(
|
||||
parent,
|
||||
Session {
|
||||
label: "label".to_string(),
|
||||
// Note we set the exp to None so we are not removing based on removal of the parent.
|
||||
expiry: None,
|
||||
// Need the other inner bits?
|
||||
// for the gracewindow.
|
||||
issued_at,
|
||||
// Who actually created this?
|
||||
issued_by,
|
||||
// What is the access scope of this session? This is
|
||||
// for auditing purposes.
|
||||
scope,
|
||||
},
|
||||
)
|
||||
),
|
||||
]);
|
||||
|
||||
server_txn
|
||||
.internal_modify(
|
||||
&filter!(f_eq("uuid", PartialValue::new_uuid(tuuid))),
|
||||
&modlist,
|
||||
)
|
||||
.expect("Failed to modify user");
|
||||
|
||||
// Still there
|
||||
|
||||
let entry = server_txn.internal_search_uuid(&tuuid).expect("failed");
|
||||
assert!(entry.attribute_equality("user_auth_token_session", &pv_parent_id));
|
||||
assert!(entry.attribute_equality("oauth2_session", &pv_session_id));
|
||||
|
||||
// Delete the oauth2 resource server.
|
||||
assert!(server_txn.internal_delete_uuid(rs_uuid).is_ok());
|
||||
|
||||
// Oauth2 Session gone.
|
||||
let entry = server_txn.internal_search_uuid(&tuuid).expect("failed");
|
||||
|
||||
// Note the uat is present still.
|
||||
assert!(entry.attribute_equality("user_auth_token_session", &pv_parent_id));
|
||||
// The oauth2 session is removed.
|
||||
assert!(!entry.attribute_equality("oauth2_session", &pv_session_id));
|
||||
|
||||
assert!(server_txn.commit().is_ok());
|
||||
}
|
||||
}
|
||||
|
|
575
kanidmd/lib/src/plugins/session.rs
Normal file
575
kanidmd/lib/src/plugins/session.rs
Normal file
|
@ -0,0 +1,575 @@
|
|||
//! This plugin maintains consistency of authenticated sessions on accounts.
|
||||
//!
|
||||
//! An example of this is that oauth2 sessions are child of user auth sessions,
|
||||
//! such than when the user auth session is terminated, then the corresponding
|
||||
//! oauth2 session should also be terminated.
|
||||
//!
|
||||
//! This plugin is also responsible for invaliding old sessions that are past
|
||||
//! their expiry.
|
||||
|
||||
use crate::event::ModifyEvent;
|
||||
use crate::plugins::Plugin;
|
||||
use crate::prelude::*;
|
||||
use std::collections::BTreeSet;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
pub struct SessionConsistency {}
|
||||
|
||||
impl Plugin for SessionConsistency {
|
||||
fn id() -> &'static str {
|
||||
"plugin_session_consistency"
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", name = "session_consistency", skip_all)]
|
||||
fn pre_modify(
|
||||
qs: &mut QueryServerWriteTransaction,
|
||||
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
|
||||
_me: &ModifyEvent,
|
||||
) -> Result<(), OperationError> {
|
||||
let curtime = qs.get_curtime();
|
||||
let curtime_odt = OffsetDateTime::unix_epoch() + curtime;
|
||||
|
||||
// We need to assert a number of properties. We must do these *in order*.
|
||||
cand.iter_mut().try_for_each(|entry| {
|
||||
// * If a UAT is past it's expiry, remove it.
|
||||
let expired: Option<BTreeSet<_>> = entry.get_ava_as_session_map("user_auth_token_session")
|
||||
.map(|sessions| {
|
||||
sessions.iter().filter_map(|(session_id, session)| {
|
||||
match &session.expiry {
|
||||
Some(exp) if exp <= &curtime_odt => {
|
||||
info!(%session_id, "Removing expired auth session");
|
||||
Some(PartialValue::Refer(*session_id))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
if let Some(expired) = expired.as_ref() {
|
||||
entry.remove_avas("user_auth_token_session", expired);
|
||||
}
|
||||
|
||||
// * If an oauth2 session is past it's expiry, remove it.
|
||||
// * If an oauth2 session is past the grace window, and no parent session exists, remove it.
|
||||
let oauth2_remove: Option<BTreeSet<_>> = entry.get_ava_as_oauth2session_map("oauth2_session").map(|oauth2_sessions| {
|
||||
// If we have oauth2 sessions, we need to be able to lookup if sessions exist in the uat.
|
||||
let sessions = entry.get_ava_as_session_map("user_auth_token_session");
|
||||
|
||||
oauth2_sessions.iter().filter_map(|(o2_session_id, session)| {
|
||||
match &session.expiry {
|
||||
Some(exp) if exp <= &curtime_odt => {
|
||||
info!(%o2_session_id, "Removing expired oauth2 session");
|
||||
Some(PartialValue::Refer(*o2_session_id))
|
||||
}
|
||||
_ => {
|
||||
// Okay, now check the issued / grace time for parent enforcement.
|
||||
if session.issued_at + GRACE_WINDOW <= curtime_odt {
|
||||
if sessions.map(|s| s.contains_key(&session.parent)).unwrap_or(false) {
|
||||
// The parent exists, go ahead
|
||||
None
|
||||
} else {
|
||||
info!(%o2_session_id, "Removing unbound oauth2 session");
|
||||
Some(PartialValue::Refer(*o2_session_id))
|
||||
}
|
||||
} else {
|
||||
// Grace window is still in effect
|
||||
None
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
if let Some(oauth2_remove) = oauth2_remove.as_ref() {
|
||||
entry.remove_avas("oauth2_session", oauth2_remove);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// use kanidm_proto::v1::PluginError;
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::event::CreateEvent;
|
||||
use crate::value::{Oauth2Session, Session};
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use uuid::uuid;
|
||||
|
||||
// Test expiry of old sessions
|
||||
|
||||
#[qs_test]
|
||||
async fn test_session_consistency_expire_old_sessions(server: &QueryServer) {
|
||||
let curtime = duration_from_epoch_now();
|
||||
let curtime_odt = OffsetDateTime::unix_epoch() + curtime;
|
||||
|
||||
let exp_curtime = curtime + Duration::from_secs(60);
|
||||
let exp_curtime_odt = OffsetDateTime::unix_epoch() + exp_curtime;
|
||||
|
||||
// Create a user
|
||||
let mut server_txn = server.write(curtime).await;
|
||||
|
||||
let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
|
||||
|
||||
let e1 = entry_init!(
|
||||
("class", Value::new_class("object")),
|
||||
("class", Value::new_class("person")),
|
||||
("class", Value::new_class("account")),
|
||||
("name", Value::new_iname("testperson1")),
|
||||
("uuid", Value::new_uuid(tuuid)),
|
||||
("description", Value::new_utf8s("testperson1")),
|
||||
("displayname", Value::new_utf8s("testperson1"))
|
||||
);
|
||||
|
||||
let ce = CreateEvent::new_internal(vec![e1]);
|
||||
assert!(server_txn.create(&ce).is_ok());
|
||||
|
||||
// Create a fake session.
|
||||
let session_id = Uuid::new_v4();
|
||||
let pv_session_id = PartialValue::new_refer(session_id);
|
||||
let expiry = Some(exp_curtime_odt);
|
||||
let issued_at = curtime_odt;
|
||||
let issued_by = IdentityId::User(tuuid);
|
||||
let scope = AccessScope::IdentityOnly;
|
||||
|
||||
let session = Value::Session(
|
||||
session_id,
|
||||
Session {
|
||||
label: "label".to_string(),
|
||||
expiry,
|
||||
// Need the other inner bits?
|
||||
// for the gracewindow.
|
||||
issued_at,
|
||||
// Who actually created this?
|
||||
issued_by,
|
||||
// What is the access scope of this session? This is
|
||||
// for auditing purposes.
|
||||
scope,
|
||||
},
|
||||
);
|
||||
|
||||
// Mod the user
|
||||
let modlist = ModifyList::new_append("user_auth_token_session", session);
|
||||
|
||||
server_txn
|
||||
.internal_modify(
|
||||
&filter!(f_eq("uuid", PartialValue::new_uuid(tuuid))),
|
||||
&modlist,
|
||||
)
|
||||
.expect("Failed to modify user");
|
||||
|
||||
// Still there
|
||||
|
||||
let entry = server_txn.internal_search_uuid(&tuuid).expect("failed");
|
||||
|
||||
assert!(entry.attribute_equality("user_auth_token_session", &pv_session_id));
|
||||
|
||||
assert!(server_txn.commit().is_ok());
|
||||
let mut server_txn = server.write(exp_curtime).await;
|
||||
|
||||
// Mod again - anything will do.
|
||||
let modlist =
|
||||
ModifyList::new_purge_and_set("description", Value::new_utf8s("test person 1 change"));
|
||||
|
||||
server_txn
|
||||
.internal_modify(
|
||||
&filter!(f_eq("uuid", PartialValue::new_uuid(tuuid))),
|
||||
&modlist,
|
||||
)
|
||||
.expect("Failed to modify user");
|
||||
|
||||
// Session gone.
|
||||
let entry = server_txn.internal_search_uuid(&tuuid).expect("failed");
|
||||
|
||||
// Note it's a not condition now.
|
||||
assert!(!entry.attribute_equality("user_auth_token_session", &pv_session_id));
|
||||
|
||||
assert!(server_txn.commit().is_ok());
|
||||
}
|
||||
|
||||
// Test expiry of old oauth2 sessions
|
||||
#[qs_test]
|
||||
async fn test_session_consistency_oauth2_expiry_cleanup(server: &QueryServer) {
|
||||
let curtime = duration_from_epoch_now();
|
||||
let curtime_odt = OffsetDateTime::unix_epoch() + curtime;
|
||||
|
||||
// Set exp to gracewindow.
|
||||
let exp_curtime = curtime + GRACE_WINDOW;
|
||||
let exp_curtime_odt = OffsetDateTime::unix_epoch() + exp_curtime;
|
||||
|
||||
// Create a user
|
||||
let mut server_txn = server.write(curtime).await;
|
||||
|
||||
let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
|
||||
let rs_uuid = Uuid::new_v4();
|
||||
|
||||
let e1 = entry_init!(
|
||||
("class", Value::new_class("object")),
|
||||
("class", Value::new_class("person")),
|
||||
("class", Value::new_class("account")),
|
||||
("name", Value::new_iname("testperson1")),
|
||||
("uuid", Value::new_uuid(tuuid)),
|
||||
("description", Value::new_utf8s("testperson1")),
|
||||
("displayname", Value::new_utf8s("testperson1"))
|
||||
);
|
||||
|
||||
let e2 = entry_init!(
|
||||
("class", Value::new_class("object")),
|
||||
("class", Value::new_class("oauth2_resource_server")),
|
||||
("class", Value::new_class("oauth2_resource_server_basic")),
|
||||
("uuid", Value::new_uuid(rs_uuid)),
|
||||
("oauth2_rs_name", Value::new_iname("test_resource_server")),
|
||||
("displayname", Value::new_utf8s("test_resource_server")),
|
||||
(
|
||||
"oauth2_rs_origin",
|
||||
Value::new_url_s("https://demo.example.com").unwrap()
|
||||
),
|
||||
// System admins
|
||||
(
|
||||
"oauth2_rs_scope_map",
|
||||
Value::new_oauthscopemap(UUID_IDM_ALL_ACCOUNTS, btreeset!["openid".to_string()])
|
||||
.expect("invalid oauthscope")
|
||||
)
|
||||
);
|
||||
|
||||
let ce = CreateEvent::new_internal(vec![e1, e2]);
|
||||
assert!(server_txn.create(&ce).is_ok());
|
||||
|
||||
// Create a fake session and oauth2 session.
|
||||
|
||||
let session_id = Uuid::new_v4();
|
||||
let pv_session_id = PartialValue::new_refer(session_id);
|
||||
|
||||
let parent = Uuid::new_v4();
|
||||
let pv_parent_id = PartialValue::new_refer(parent);
|
||||
let expiry = Some(exp_curtime_odt);
|
||||
let issued_at = curtime_odt;
|
||||
let issued_by = IdentityId::User(tuuid);
|
||||
let scope = AccessScope::IdentityOnly;
|
||||
|
||||
// Mod the user
|
||||
let modlist = modlist!([
|
||||
Modify::Present(
|
||||
"oauth2_session".into(),
|
||||
Value::Oauth2Session(
|
||||
session_id,
|
||||
Oauth2Session {
|
||||
parent,
|
||||
// Set to the exp window.
|
||||
expiry,
|
||||
issued_at,
|
||||
rs_uuid,
|
||||
},
|
||||
)
|
||||
),
|
||||
Modify::Present(
|
||||
"user_auth_token_session".into(),
|
||||
Value::Session(
|
||||
parent,
|
||||
Session {
|
||||
label: "label".to_string(),
|
||||
// Note we set the exp to None so we are not removing based on removal of the parent.
|
||||
expiry: None,
|
||||
// Need the other inner bits?
|
||||
// for the gracewindow.
|
||||
issued_at,
|
||||
// Who actually created this?
|
||||
issued_by,
|
||||
// What is the access scope of this session? This is
|
||||
// for auditing purposes.
|
||||
scope,
|
||||
},
|
||||
)
|
||||
),
|
||||
]);
|
||||
|
||||
server_txn
|
||||
.internal_modify(
|
||||
&filter!(f_eq("uuid", PartialValue::new_uuid(tuuid))),
|
||||
&modlist,
|
||||
)
|
||||
.expect("Failed to modify user");
|
||||
|
||||
// Still there
|
||||
|
||||
let entry = server_txn.internal_search_uuid(&tuuid).expect("failed");
|
||||
|
||||
assert!(entry.attribute_equality("user_auth_token_session", &pv_parent_id));
|
||||
assert!(entry.attribute_equality("oauth2_session", &pv_session_id));
|
||||
|
||||
assert!(server_txn.commit().is_ok());
|
||||
|
||||
// Note as we are now past exp time, the oauth2 session will be removed, but the uat session
|
||||
// will remain.
|
||||
let mut server_txn = server.write(exp_curtime).await;
|
||||
|
||||
// Mod again - anything will do.
|
||||
let modlist =
|
||||
ModifyList::new_purge_and_set("description", Value::new_utf8s("test person 1 change"));
|
||||
|
||||
server_txn
|
||||
.internal_modify(
|
||||
&filter!(f_eq("uuid", PartialValue::new_uuid(tuuid))),
|
||||
&modlist,
|
||||
)
|
||||
.expect("Failed to modify user");
|
||||
|
||||
// Session gone.
|
||||
let entry = server_txn.internal_search_uuid(&tuuid).expect("failed");
|
||||
|
||||
// Note the uat is still present
|
||||
assert!(entry.attribute_equality("user_auth_token_session", &pv_parent_id));
|
||||
// Note it's a not condition now.
|
||||
assert!(!entry.attribute_equality("oauth2_session", &pv_session_id));
|
||||
|
||||
assert!(server_txn.commit().is_ok());
|
||||
}
|
||||
|
||||
// test removal of a session removes related oauth2 sessions.
|
||||
#[qs_test]
|
||||
async fn test_session_consistency_oauth2_removed_by_parent(server: &QueryServer) {
|
||||
let curtime = duration_from_epoch_now();
|
||||
let curtime_odt = OffsetDateTime::unix_epoch() + curtime;
|
||||
let exp_curtime = curtime + GRACE_WINDOW;
|
||||
|
||||
// Create a user
|
||||
let mut server_txn = server.write(curtime).await;
|
||||
|
||||
let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
|
||||
let rs_uuid = Uuid::new_v4();
|
||||
|
||||
let e1 = entry_init!(
|
||||
("class", Value::new_class("object")),
|
||||
("class", Value::new_class("person")),
|
||||
("class", Value::new_class("account")),
|
||||
("name", Value::new_iname("testperson1")),
|
||||
("uuid", Value::new_uuid(tuuid)),
|
||||
("description", Value::new_utf8s("testperson1")),
|
||||
("displayname", Value::new_utf8s("testperson1"))
|
||||
);
|
||||
|
||||
let e2 = entry_init!(
|
||||
("class", Value::new_class("object")),
|
||||
("class", Value::new_class("oauth2_resource_server")),
|
||||
("class", Value::new_class("oauth2_resource_server_basic")),
|
||||
("uuid", Value::new_uuid(rs_uuid)),
|
||||
("oauth2_rs_name", Value::new_iname("test_resource_server")),
|
||||
("displayname", Value::new_utf8s("test_resource_server")),
|
||||
(
|
||||
"oauth2_rs_origin",
|
||||
Value::new_url_s("https://demo.example.com").unwrap()
|
||||
),
|
||||
// System admins
|
||||
(
|
||||
"oauth2_rs_scope_map",
|
||||
Value::new_oauthscopemap(UUID_IDM_ALL_ACCOUNTS, btreeset!["openid".to_string()])
|
||||
.expect("invalid oauthscope")
|
||||
)
|
||||
);
|
||||
|
||||
let ce = CreateEvent::new_internal(vec![e1, e2]);
|
||||
assert!(server_txn.create(&ce).is_ok());
|
||||
|
||||
// Create a fake session and oauth2 session.
|
||||
|
||||
let session_id = Uuid::new_v4();
|
||||
let pv_session_id = PartialValue::new_refer(session_id);
|
||||
|
||||
let parent = Uuid::new_v4();
|
||||
let pv_parent_id = PartialValue::new_refer(parent);
|
||||
let issued_at = curtime_odt;
|
||||
let issued_by = IdentityId::User(tuuid);
|
||||
let scope = AccessScope::IdentityOnly;
|
||||
|
||||
// Mod the user
|
||||
let modlist = modlist!([
|
||||
Modify::Present(
|
||||
"oauth2_session".into(),
|
||||
Value::Oauth2Session(
|
||||
session_id,
|
||||
Oauth2Session {
|
||||
parent,
|
||||
// Note we set the exp to None so we are not removing based on exp
|
||||
expiry: None,
|
||||
issued_at,
|
||||
rs_uuid,
|
||||
},
|
||||
)
|
||||
),
|
||||
Modify::Present(
|
||||
"user_auth_token_session".into(),
|
||||
Value::Session(
|
||||
parent,
|
||||
Session {
|
||||
label: "label".to_string(),
|
||||
// Note we set the exp to None so we are not removing based on removal of the parent.
|
||||
expiry: None,
|
||||
// Need the other inner bits?
|
||||
// for the gracewindow.
|
||||
issued_at,
|
||||
// Who actually created this?
|
||||
issued_by,
|
||||
// What is the access scope of this session? This is
|
||||
// for auditing purposes.
|
||||
scope,
|
||||
},
|
||||
)
|
||||
),
|
||||
]);
|
||||
|
||||
server_txn
|
||||
.internal_modify(
|
||||
&filter!(f_eq("uuid", PartialValue::new_uuid(tuuid))),
|
||||
&modlist,
|
||||
)
|
||||
.expect("Failed to modify user");
|
||||
|
||||
// Still there
|
||||
|
||||
let entry = server_txn.internal_search_uuid(&tuuid).expect("failed");
|
||||
|
||||
assert!(entry.attribute_equality("user_auth_token_session", &pv_parent_id));
|
||||
assert!(entry.attribute_equality("oauth2_session", &pv_session_id));
|
||||
|
||||
// We need the time to be past grace_window.
|
||||
assert!(server_txn.commit().is_ok());
|
||||
let mut server_txn = server.write(exp_curtime).await;
|
||||
|
||||
// Mod again - remove the parent session.
|
||||
let modlist = ModifyList::new_remove("user_auth_token_session", pv_parent_id.clone());
|
||||
|
||||
server_txn
|
||||
.internal_modify(
|
||||
&filter!(f_eq("uuid", PartialValue::new_uuid(tuuid))),
|
||||
&modlist,
|
||||
)
|
||||
.expect("Failed to modify user");
|
||||
|
||||
// Session gone.
|
||||
let entry = server_txn.internal_search_uuid(&tuuid).expect("failed");
|
||||
|
||||
// Note the uat is removed
|
||||
assert!(!entry.attribute_equality("user_auth_token_session", &pv_parent_id));
|
||||
// The oauth2 session is also removed.
|
||||
assert!(!entry.attribute_equality("oauth2_session", &pv_session_id));
|
||||
|
||||
assert!(server_txn.commit().is_ok());
|
||||
}
|
||||
|
||||
// Test if an oauth2 session exists, the grace window passes and it's UAT doesn't exist.
|
||||
#[qs_test]
|
||||
async fn test_session_consistency_oauth2_grace_window_past(server: &QueryServer) {
|
||||
let curtime = duration_from_epoch_now();
|
||||
let curtime_odt = OffsetDateTime::unix_epoch() + curtime;
|
||||
|
||||
// Set exp to gracewindow.
|
||||
let exp_curtime = curtime + GRACE_WINDOW;
|
||||
// let exp_curtime_odt = OffsetDateTime::unix_epoch() + exp_curtime;
|
||||
|
||||
// Create a user
|
||||
let mut server_txn = server.write(curtime).await;
|
||||
|
||||
let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
|
||||
let rs_uuid = Uuid::new_v4();
|
||||
|
||||
let e1 = entry_init!(
|
||||
("class", Value::new_class("object")),
|
||||
("class", Value::new_class("person")),
|
||||
("class", Value::new_class("account")),
|
||||
("name", Value::new_iname("testperson1")),
|
||||
("uuid", Value::new_uuid(tuuid)),
|
||||
("description", Value::new_utf8s("testperson1")),
|
||||
("displayname", Value::new_utf8s("testperson1"))
|
||||
);
|
||||
|
||||
let e2 = entry_init!(
|
||||
("class", Value::new_class("object")),
|
||||
("class", Value::new_class("oauth2_resource_server")),
|
||||
("class", Value::new_class("oauth2_resource_server_basic")),
|
||||
("uuid", Value::new_uuid(rs_uuid)),
|
||||
("oauth2_rs_name", Value::new_iname("test_resource_server")),
|
||||
("displayname", Value::new_utf8s("test_resource_server")),
|
||||
(
|
||||
"oauth2_rs_origin",
|
||||
Value::new_url_s("https://demo.example.com").unwrap()
|
||||
),
|
||||
// System admins
|
||||
(
|
||||
"oauth2_rs_scope_map",
|
||||
Value::new_oauthscopemap(UUID_IDM_ALL_ACCOUNTS, btreeset!["openid".to_string()])
|
||||
.expect("invalid oauthscope")
|
||||
)
|
||||
);
|
||||
|
||||
let ce = CreateEvent::new_internal(vec![e1, e2]);
|
||||
assert!(server_txn.create(&ce).is_ok());
|
||||
|
||||
// Create a fake session.
|
||||
let session_id = Uuid::new_v4();
|
||||
let pv_session_id = PartialValue::new_refer(session_id);
|
||||
|
||||
let parent = Uuid::new_v4();
|
||||
let issued_at = curtime_odt;
|
||||
|
||||
let session = Value::Oauth2Session(
|
||||
session_id,
|
||||
Oauth2Session {
|
||||
parent,
|
||||
// Note we set the exp to None so we are asserting the removal is due to the lack
|
||||
// of the parent session.
|
||||
expiry: None,
|
||||
issued_at,
|
||||
rs_uuid,
|
||||
},
|
||||
);
|
||||
|
||||
// Mod the user
|
||||
let modlist = ModifyList::new_append("oauth2_session", session);
|
||||
|
||||
server_txn
|
||||
.internal_modify(
|
||||
&filter!(f_eq("uuid", PartialValue::new_uuid(tuuid))),
|
||||
&modlist,
|
||||
)
|
||||
.expect("Failed to modify user");
|
||||
|
||||
// Still there
|
||||
|
||||
let entry = server_txn.internal_search_uuid(&tuuid).expect("failed");
|
||||
|
||||
assert!(entry.attribute_equality("oauth2_session", &pv_session_id));
|
||||
|
||||
assert!(server_txn.commit().is_ok());
|
||||
|
||||
// Note the exp_curtime now is past the gracewindow. This will trigger
|
||||
// consistency to purge the un-matched session.
|
||||
let mut server_txn = server.write(exp_curtime).await;
|
||||
|
||||
// Mod again - anything will do.
|
||||
let modlist =
|
||||
ModifyList::new_purge_and_set("description", Value::new_utf8s("test person 1 change"));
|
||||
|
||||
server_txn
|
||||
.internal_modify(
|
||||
&filter!(f_eq("uuid", PartialValue::new_uuid(tuuid))),
|
||||
&modlist,
|
||||
)
|
||||
.expect("Failed to modify user");
|
||||
|
||||
// Session gone.
|
||||
let entry = server_txn.internal_search_uuid(&tuuid).expect("failed");
|
||||
|
||||
// Note it's a not condition now.
|
||||
assert!(!entry.attribute_equality("oauth2_session", &pv_session_id));
|
||||
|
||||
assert!(server_txn.commit().is_ok());
|
||||
}
|
||||
}
|
|
@ -193,6 +193,7 @@ impl SchemaAttribute {
|
|||
SyntaxType::DeviceKey => matches!(v, PartialValue::DeviceKey(_)),
|
||||
// Allow refer types.
|
||||
SyntaxType::Session => matches!(v, PartialValue::Refer(_)),
|
||||
SyntaxType::Oauth2Session => matches!(v, PartialValue::Refer(_)),
|
||||
// These are just insensitive string lookups on the hex-ified kid.
|
||||
SyntaxType::JwsKeyEs256 => matches!(v, PartialValue::Iutf8(_)),
|
||||
SyntaxType::JwsKeyRs256 => matches!(v, PartialValue::Iutf8(_)),
|
||||
|
@ -239,6 +240,7 @@ impl SchemaAttribute {
|
|||
SyntaxType::Passkey => matches!(v, Value::Passkey(_, _, _)),
|
||||
SyntaxType::DeviceKey => matches!(v, Value::DeviceKey(_, _, _)),
|
||||
SyntaxType::Session => matches!(v, Value::Session(_, _)),
|
||||
SyntaxType::Oauth2Session => matches!(v, Value::Oauth2Session(_, _)),
|
||||
SyntaxType::JwsKeyEs256 => matches!(v, Value::JwsKeyEs256(_)),
|
||||
SyntaxType::JwsKeyRs256 => matches!(v, Value::JwsKeyRs256(_)),
|
||||
};
|
||||
|
@ -582,7 +584,10 @@ impl<'a> SchemaWriteTransaction<'a> {
|
|||
// No, they'll over-write each other ... but we do need name uniqueness.
|
||||
attributetypes.into_iter().for_each(|a| {
|
||||
// Update the unique and ref caches.
|
||||
if a.syntax == SyntaxType::ReferenceUuid || a.syntax == SyntaxType::OauthScopeMap
|
||||
if a.syntax == SyntaxType::ReferenceUuid ||
|
||||
a.syntax == SyntaxType::OauthScopeMap ||
|
||||
// So that when an rs is removed we trigger removal of the sessions.
|
||||
a.syntax == SyntaxType::Oauth2Session
|
||||
// May not need to be a ref type since it doesn't have external links/impact?
|
||||
// || a.syntax == SyntaxType::Session
|
||||
{
|
||||
|
|
|
@ -90,6 +90,7 @@ pub struct QueryServerWriteTransaction<'a> {
|
|||
committed: bool,
|
||||
phase: CowCellWriteTxn<'a, ServerPhase>,
|
||||
d_info: CowCellWriteTxn<'a, DomainInfo>,
|
||||
curtime: Duration,
|
||||
cid: Cid,
|
||||
be_txn: BackendWriteTransaction<'a>,
|
||||
schema: SchemaWriteTransaction<'a>,
|
||||
|
@ -515,6 +516,7 @@ pub trait QueryServerTransaction<'a> {
|
|||
SyntaxType::Session => Err(OperationError::InvalidAttribute("Session Values can not be supplied through modification".to_string())),
|
||||
SyntaxType::JwsKeyEs256 => Err(OperationError::InvalidAttribute("JwsKeyEs256 Values can not be supplied through modification".to_string())),
|
||||
SyntaxType::JwsKeyRs256 => Err(OperationError::InvalidAttribute("JwsKeyRs256 Values can not be supplied through modification".to_string())),
|
||||
SyntaxType::Oauth2Session => Err(OperationError::InvalidAttribute("Oauth2Session Values can not be supplied through modification".to_string())),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
@ -576,7 +578,10 @@ pub trait QueryServerTransaction<'a> {
|
|||
// ⚠️ Any types here need to also be added to update_attributes in
|
||||
// schema.rs for reference type / cache awareness during referential
|
||||
// integrity processing. Exceptions are self-contained value types!
|
||||
SyntaxType::ReferenceUuid | SyntaxType::OauthScopeMap | SyntaxType::Session => {
|
||||
SyntaxType::ReferenceUuid
|
||||
| SyntaxType::OauthScopeMap
|
||||
| SyntaxType::Session
|
||||
| SyntaxType::Oauth2Session => {
|
||||
// See comments above.
|
||||
PartialValue::new_refer_s(value)
|
||||
.or_else(|| {
|
||||
|
@ -1004,7 +1009,7 @@ impl QueryServer {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn write(&self, ts: Duration) -> QueryServerWriteTransaction<'_> {
|
||||
pub async fn write(&self, curtime: Duration) -> QueryServerWriteTransaction<'_> {
|
||||
// Guarantee we are the only writer on the thread pool
|
||||
#[allow(clippy::expect_used)]
|
||||
let write_ticket = self
|
||||
|
@ -1026,8 +1031,10 @@ impl QueryServer {
|
|||
let phase = self.phase.write();
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
let ts_max = be_txn.get_db_ts_max(ts).expect("Unable to get db_ts_max");
|
||||
let cid = Cid::new_lamport(self.s_uuid, d_info.d_uuid, ts, &ts_max);
|
||||
let ts_max = be_txn
|
||||
.get_db_ts_max(curtime)
|
||||
.expect("Unable to get db_ts_max");
|
||||
let cid = Cid::new_lamport(self.s_uuid, d_info.d_uuid, curtime, &ts_max);
|
||||
|
||||
QueryServerWriteTransaction {
|
||||
// I think this is *not* needed, because commit is mut self which should
|
||||
|
@ -1039,6 +1046,7 @@ impl QueryServer {
|
|||
committed: false,
|
||||
phase,
|
||||
d_info,
|
||||
curtime,
|
||||
cid,
|
||||
be_txn,
|
||||
schema: schema_write,
|
||||
|
@ -1174,6 +1182,10 @@ impl QueryServer {
|
|||
}
|
||||
|
||||
impl<'a> QueryServerWriteTransaction<'a> {
|
||||
pub(crate) fn get_curtime(&self) -> Duration {
|
||||
self.curtime
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub fn create(&mut self, ce: &CreateEvent) -> Result<(), OperationError> {
|
||||
// The create event is a raw, read only representation of the request
|
||||
|
@ -2371,6 +2383,15 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
self.delete(&de)
|
||||
}
|
||||
|
||||
pub fn internal_delete_uuid(&mut self, target_uuid: Uuid) -> Result<(), OperationError> {
|
||||
let filter = filter!(f_eq("uuid", PartialValue::new_uuid(target_uuid)));
|
||||
let f_valid = filter
|
||||
.validate(self.get_schema())
|
||||
.map_err(OperationError::SchemaViolation)?;
|
||||
let de = DeleteEvent::new_internal(f_valid);
|
||||
self.delete(&de)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub fn internal_modify(
|
||||
&mut self,
|
||||
|
@ -2653,6 +2674,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
JSON_SCHEMA_ATTR_API_TOKEN_SESSION,
|
||||
JSON_SCHEMA_ATTR_OAUTH2_RS_SUP_SCOPE_MAP,
|
||||
JSON_SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION,
|
||||
JSON_SCHEMA_ATTR_OAUTH2_SESSION,
|
||||
JSON_SCHEMA_ATTR_NSUNIQUEID,
|
||||
JSON_SCHEMA_ATTR_OAUTH2_PREFER_SHORT_USERNAME,
|
||||
JSON_SCHEMA_ATTR_SYNC_TOKEN_SESSION,
|
||||
|
|
|
@ -188,6 +188,7 @@ pub enum SyntaxType {
|
|||
Session = 25,
|
||||
JwsKeyEs256 = 26,
|
||||
JwsKeyRs256 = 27,
|
||||
Oauth2Session = 28,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for SyntaxType {
|
||||
|
@ -225,6 +226,7 @@ impl TryFrom<&str> for SyntaxType {
|
|||
"SESSION" => Ok(SyntaxType::Session),
|
||||
"JWS_KEY_ES256" => Ok(SyntaxType::JwsKeyEs256),
|
||||
"JWS_KEY_RS256" => Ok(SyntaxType::JwsKeyRs256),
|
||||
"OAUTH2SESSION" => Ok(SyntaxType::Oauth2Session),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
@ -263,6 +265,7 @@ impl TryFrom<u16> for SyntaxType {
|
|||
25 => Ok(SyntaxType::Session),
|
||||
26 => Ok(SyntaxType::JwsKeyEs256),
|
||||
27 => Ok(SyntaxType::JwsKeyRs256),
|
||||
28 => Ok(SyntaxType::Oauth2Session),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
@ -299,6 +302,7 @@ impl fmt::Display for SyntaxType {
|
|||
SyntaxType::Session => "SESSION",
|
||||
SyntaxType::JwsKeyEs256 => "JWS_KEY_ES256",
|
||||
SyntaxType::JwsKeyRs256 => "JWS_KEY_RS256",
|
||||
SyntaxType::Oauth2Session => "OAUTH2SESSION",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -750,6 +754,14 @@ pub struct Session {
|
|||
pub scope: AccessScope,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Oauth2Session {
|
||||
pub parent: Uuid,
|
||||
pub expiry: Option<OffsetDateTime>,
|
||||
pub issued_at: OffsetDateTime,
|
||||
pub rs_uuid: Uuid,
|
||||
}
|
||||
|
||||
/// A value is a complete unit of data for an attribute. It is made up of a PartialValue, which is
|
||||
/// used for selection, filtering, searching, matching etc. It also contains supplemental data
|
||||
/// which may be stored inside of the Value, such as credential secrets, blobs etc.
|
||||
|
@ -794,6 +806,7 @@ pub enum Value {
|
|||
|
||||
TrustedDeviceEnrollment(Uuid),
|
||||
Session(Uuid, Session),
|
||||
Oauth2Session(Uuid, Oauth2Session),
|
||||
|
||||
JwsKeyEs256(JwsSigner),
|
||||
JwsKeyRs256(JwsSigner),
|
||||
|
@ -1329,10 +1342,12 @@ impl Value {
|
|||
|
||||
// We need a seperate to-ref_uuid to distinguish from normal uuids
|
||||
// in refint plugin.
|
||||
pub fn to_ref_uuid(&self) -> Option<&Uuid> {
|
||||
pub fn to_ref_uuid(&self) -> Option<Uuid> {
|
||||
match &self {
|
||||
Value::Refer(u) => Some(u),
|
||||
Value::OauthScopeMap(u, _) => Some(u),
|
||||
Value::Refer(u) => Some(*u),
|
||||
Value::OauthScopeMap(u, _) => Some(*u),
|
||||
// We need to assert that our reference to our rs exists.
|
||||
Value::Oauth2Session(_, m) => Some(m.rs_uuid),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ use crate::credential::Credential;
|
|||
use crate::prelude::*;
|
||||
use crate::repl::cid::Cid;
|
||||
use crate::schema::SchemaAttribute;
|
||||
use crate::value::{Address, IntentTokenState, Session};
|
||||
use crate::value::{Address, IntentTokenState, Oauth2Session, Session};
|
||||
|
||||
mod address;
|
||||
mod binary;
|
||||
|
@ -56,7 +56,7 @@ pub use self::nsuniqueid::ValueSetNsUniqueId;
|
|||
pub use self::oauth::{ValueSetOauthScope, ValueSetOauthScopeMap};
|
||||
pub use self::restricted::ValueSetRestricted;
|
||||
pub use self::secret::ValueSetSecret;
|
||||
pub use self::session::ValueSetSession;
|
||||
pub use self::session::{ValueSetOauth2Session, ValueSetSession};
|
||||
pub use self::spn::ValueSetSpn;
|
||||
pub use self::ssh::ValueSetSshKey;
|
||||
pub use self::syntax::ValueSetSyntax;
|
||||
|
@ -471,6 +471,11 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
|
|||
None
|
||||
}
|
||||
|
||||
fn as_oauth2session_map(&self) -> Option<&BTreeMap<Uuid, Oauth2Session>> {
|
||||
debug_assert!(false);
|
||||
None
|
||||
}
|
||||
|
||||
fn to_jws_key_es256_single(&self) -> Option<&JwsSigner> {
|
||||
debug_assert!(false);
|
||||
None
|
||||
|
@ -546,6 +551,7 @@ pub fn from_result_value_iter(
|
|||
| Value::DeviceKey(_, _, _)
|
||||
| Value::TrustedDeviceEnrollment(_)
|
||||
| Value::Session(_, _)
|
||||
| Value::Oauth2Session(_, _)
|
||||
| Value::JwsKeyEs256(_)
|
||||
| Value::JwsKeyRs256(_) => {
|
||||
debug_assert!(false);
|
||||
|
@ -601,6 +607,7 @@ pub fn from_value_iter(mut iter: impl Iterator<Item = Value>) -> Result<ValueSet
|
|||
Value::JwsKeyEs256(k) => ValueSetJwsKeyEs256::new(k),
|
||||
Value::JwsKeyRs256(k) => ValueSetJwsKeyRs256::new(k),
|
||||
Value::Session(u, m) => ValueSetSession::new(u, m),
|
||||
Value::Oauth2Session(u, m) => ValueSetOauth2Session::new(u, m),
|
||||
Value::PhoneNumber(_, _) | Value::TrustedDeviceEnrollment(_) => {
|
||||
debug_assert!(false);
|
||||
return Err(OperationError::InvalidValueState);
|
||||
|
@ -644,6 +651,7 @@ pub fn from_db_valueset_v2(dbvs: DbValueSetV2) -> Result<ValueSet, OperationErro
|
|||
DbValueSetV2::Passkey(set) => ValueSetPasskey::from_dbvs2(set),
|
||||
DbValueSetV2::DeviceKey(set) => ValueSetDeviceKey::from_dbvs2(set),
|
||||
DbValueSetV2::Session(set) => ValueSetSession::from_dbvs2(set),
|
||||
DbValueSetV2::Oauth2Session(set) => ValueSetOauth2Session::from_dbvs2(set),
|
||||
DbValueSetV2::JwsKeyEs256(set) => ValueSetJwsKeyEs256::from_dbvs2(&set),
|
||||
DbValueSetV2::JwsKeyRs256(set) => ValueSetJwsKeyEs256::from_dbvs2(&set),
|
||||
DbValueSetV2::PhoneNumber(_, _) | DbValueSetV2::TrustedDeviceEnrollment(_) => {
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
use std::collections::btree_map::Entry as BTreeEntry;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::be::dbvalue::{DbValueAccessScopeV1, DbValueIdentityId, DbValueSession};
|
||||
use crate::be::dbvalue::{
|
||||
DbValueAccessScopeV1, DbValueIdentityId, DbValueOauth2Session, DbValueSession,
|
||||
};
|
||||
use crate::identity::{AccessScope, IdentityId};
|
||||
use crate::prelude::*;
|
||||
use crate::schema::SchemaAttribute;
|
||||
use crate::value::Session;
|
||||
use crate::value::{Oauth2Session, Session};
|
||||
use crate::valueset::{uuid_to_proto_string, DbValueSetV2, ValueSet};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -250,3 +252,276 @@ impl ValueSetT for ValueSetSession {
|
|||
Some(Box::new(self.map.keys().copied()))
|
||||
}
|
||||
}
|
||||
|
||||
// == oauth2 session ==
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ValueSetOauth2Session {
|
||||
map: BTreeMap<Uuid, Oauth2Session>,
|
||||
// this is a "filter" to tell us if as rs_id is used anywhere
|
||||
// in this set. The reason is so that we don't do O(n) searches
|
||||
// on a refer if it's not in this set. The alternate approach is
|
||||
// an index on these maps, but its more work to mantain for a rare
|
||||
// situation where we actually want to query rs_uuid -> sessions.
|
||||
rs_filter: BTreeSet<Uuid>,
|
||||
}
|
||||
|
||||
impl ValueSetOauth2Session {
|
||||
pub fn new(u: Uuid, m: Oauth2Session) -> Box<Self> {
|
||||
let mut map = BTreeMap::new();
|
||||
let mut rs_filter = BTreeSet::new();
|
||||
rs_filter.insert(m.rs_uuid);
|
||||
map.insert(u, m);
|
||||
Box::new(ValueSetOauth2Session { map, rs_filter })
|
||||
}
|
||||
|
||||
pub fn push(&mut self, u: Uuid, m: Oauth2Session) -> bool {
|
||||
self.rs_filter.insert(m.rs_uuid);
|
||||
self.map.insert(u, m).is_none()
|
||||
}
|
||||
|
||||
pub fn from_dbvs2(data: Vec<DbValueOauth2Session>) -> Result<ValueSet, OperationError> {
|
||||
let mut rs_filter = BTreeSet::new();
|
||||
let map = data
|
||||
.into_iter()
|
||||
.filter_map(|dbv| {
|
||||
match dbv {
|
||||
DbValueOauth2Session::V1 {
|
||||
refer,
|
||||
parent,
|
||||
expiry,
|
||||
issued_at,
|
||||
rs_uuid,
|
||||
} => {
|
||||
// Convert things.
|
||||
let issued_at = OffsetDateTime::parse(issued_at, time::Format::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()?;
|
||||
|
||||
// This is a bit annoying. In the case we can't parse the optional
|
||||
// expiry, we need to NOT return the session so that it's immediately
|
||||
// invalidated. To do this we have to invert some of the options involved
|
||||
// here.
|
||||
let expiry = expiry
|
||||
.map(|e_inner| {
|
||||
OffsetDateTime::parse(e_inner, time::Format::Rfc3339)
|
||||
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
|
||||
// We now have an
|
||||
// Option<Result<ODT, _>>
|
||||
})
|
||||
.transpose()
|
||||
// Result<Option<ODT>, _>
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
?e,
|
||||
"Invalidating session {} due to invalid expiry timestamp",
|
||||
refer
|
||||
)
|
||||
})
|
||||
// Option<Option<ODT>>
|
||||
.ok()?;
|
||||
|
||||
// Insert to the rs_filter.
|
||||
rs_filter.insert(rs_uuid);
|
||||
Some((
|
||||
refer,
|
||||
Oauth2Session {
|
||||
parent,
|
||||
expiry,
|
||||
issued_at,
|
||||
rs_uuid,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(Box::new(ValueSetOauth2Session { map, rs_filter }))
|
||||
}
|
||||
|
||||
// We need to allow this, because rust doesn't allow us to impl FromIterator on foreign
|
||||
// types, and tuples are always foreign.
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
|
||||
where
|
||||
T: IntoIterator<Item = (Uuid, Oauth2Session)>,
|
||||
{
|
||||
let mut rs_filter = BTreeSet::new();
|
||||
let map = iter
|
||||
.into_iter()
|
||||
.map(|(u, m)| {
|
||||
rs_filter.insert(m.rs_uuid);
|
||||
(u, m)
|
||||
})
|
||||
.collect();
|
||||
Some(Box::new(ValueSetOauth2Session { map, rs_filter }))
|
||||
}
|
||||
}
|
||||
|
||||
impl ValueSetT for ValueSetOauth2Session {
|
||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||
match value {
|
||||
Value::Oauth2Session(u, m) => {
|
||||
if let BTreeEntry::Vacant(e) = self.map.entry(u) {
|
||||
self.rs_filter.insert(m.rs_uuid);
|
||||
e.insert(m);
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
_ => Err(OperationError::InvalidValueState),
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.rs_filter.clear();
|
||||
self.map.clear();
|
||||
}
|
||||
|
||||
fn remove(&mut self, pv: &PartialValue) -> bool {
|
||||
match pv {
|
||||
PartialValue::Refer(u) => {
|
||||
let found = self.map.remove(u).is_some();
|
||||
if !found {
|
||||
// Perhaps the reference id is an rs_uuid?
|
||||
if self.rs_filter.contains(u) {
|
||||
// It's there, so we need to do a more costly retain operation over the values.
|
||||
self.map.retain(|_, m| m.rs_uuid != *u);
|
||||
self.rs_filter.remove(u);
|
||||
// We removed something, so yeeeet.
|
||||
true
|
||||
} else {
|
||||
// It's not in the rs_filter or the map, false.
|
||||
false
|
||||
}
|
||||
} else {
|
||||
// We found it in the map, true
|
||||
true
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn contains(&self, pv: &PartialValue) -> bool {
|
||||
match pv {
|
||||
PartialValue::Refer(u) => self.map.contains_key(u) || self.rs_filter.contains(u),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn substring(&self, _pv: &PartialValue) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn lessthan(&self, _pv: &PartialValue) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.map.len()
|
||||
}
|
||||
|
||||
fn generate_idx_eq_keys(&self) -> Vec<String> {
|
||||
self.map
|
||||
.keys()
|
||||
.map(|u| u.as_hyphenated().to_string())
|
||||
// We also refer to our rs_uuid's.
|
||||
.chain(self.rs_filter.iter().map(|u| u.as_hyphenated().to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn syntax(&self) -> SyntaxType {
|
||||
SyntaxType::Oauth2Session
|
||||
}
|
||||
|
||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||
Box::new(
|
||||
self.map
|
||||
.iter()
|
||||
.map(|(u, m)| format!("{}: {:?}", uuid_to_proto_string(*u), m)),
|
||||
)
|
||||
}
|
||||
|
||||
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
||||
DbValueSetV2::Oauth2Session(
|
||||
self.map
|
||||
.iter()
|
||||
.map(|(u, m)| DbValueOauth2Session::V1 {
|
||||
refer: *u,
|
||||
parent: m.parent,
|
||||
expiry: m.expiry.map(|odt| {
|
||||
debug_assert!(odt.offset() == time::UtcOffset::UTC);
|
||||
odt.format(time::Format::Rfc3339)
|
||||
}),
|
||||
issued_at: {
|
||||
debug_assert!(m.issued_at.offset() == time::UtcOffset::UTC);
|
||||
m.issued_at.format(time::Format::Rfc3339)
|
||||
},
|
||||
rs_uuid: m.rs_uuid,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
|
||||
Box::new(self.map.keys().cloned().map(PartialValue::Refer))
|
||||
}
|
||||
|
||||
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
|
||||
Box::new(
|
||||
self.map
|
||||
.iter()
|
||||
.map(|(u, m)| Value::Oauth2Session(*u, m.clone())),
|
||||
)
|
||||
}
|
||||
|
||||
fn equal(&self, other: &ValueSet) -> bool {
|
||||
if let Some(other) = other.as_oauth2session_map() {
|
||||
&self.map == other
|
||||
} else {
|
||||
debug_assert!(false);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
|
||||
if let Some(b) = other.as_oauth2session_map() {
|
||||
// Merge the rs_filters.
|
||||
// We have to do this without the mergemap macro so that rs_filter
|
||||
// is updated.
|
||||
b.iter().for_each(|(k, v)| {
|
||||
if !self.map.contains_key(k) {
|
||||
self.rs_filter.insert(v.rs_uuid);
|
||||
self.map.insert(k.clone(), v.clone());
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
} else {
|
||||
debug_assert!(false);
|
||||
Err(OperationError::InvalidValueState)
|
||||
}
|
||||
}
|
||||
|
||||
fn as_oauth2session_map(&self) -> Option<&BTreeMap<Uuid, Oauth2Session>> {
|
||||
Some(&self.map)
|
||||
}
|
||||
|
||||
fn as_ref_uuid_iter(&self) -> Option<Box<dyn Iterator<Item = Uuid> + '_>> {
|
||||
// This is what ties us as a type that can be refint checked. We need to
|
||||
// bind to our resource servers, not our ids!
|
||||
Some(Box::new(self.map.values().map(|m| &m.rs_uuid).copied()))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue