mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
Start to add reauth capabilities (#1398)
Cleanup token storage, add some design notes, and improve some of the process of setting up session scopes.
This commit is contained in:
parent
6f6189fff8
commit
dd761c9a45
116
kanidm_book/src/developers/designs/elevated_priv_mode.md
Normal file
116
kanidm_book/src/developers/designs/elevated_priv_mode.md
Normal file
|
@ -0,0 +1,116 @@
|
|||
# Elevation of Privilege Inside User Sessions
|
||||
|
||||
To improve user experience, we need to allow long lived sessions in browsers. This is especially
|
||||
important as a single sign on system, users tend to be associated 1 to 1 with devices, and by having
|
||||
longer lived sessions, they have a smoother experience.
|
||||
|
||||
However, we also don't want user sessions to have unbound write permissions for the entire (possibly
|
||||
unlimited) duration of their session.
|
||||
|
||||
Prior art for this is github, which has unbounded sessions on machines and requests a
|
||||
re-authentication when a modifying or sensitive action is to occur.
|
||||
|
||||
For us to implement this will require some changes to how we manage sessions.
|
||||
|
||||
## Session Issuance
|
||||
|
||||
- ISSUE: Sessions are issued identically for service-accounts and persons
|
||||
- CHANGE: service-accounts require a hard/short session expiry limit and always have elevated
|
||||
permissions
|
||||
- CHANGE: persons require no session expiry and must request elevation for privs.
|
||||
|
||||
- ISSUE: Sessions currently indicate all read-write types as the same access scope type.
|
||||
- CHANGE: Split sessions to show rwalways, rwcapable, rwactive
|
||||
|
||||
- ISSUE: Sessions currently are recorded identically between service-accounts, persons, and api
|
||||
tokens
|
||||
- CHANGE: Change the session storage types to have unique session types for these ✅
|
||||
|
||||
- ISSUE: Access Scope types are confused by api session using the same types.
|
||||
- CHANGE: Use access scope only as the end result of current effective permission calculation and
|
||||
not as a method to convert to anything else. ✅
|
||||
|
||||
AccessScope { ReadOnly, ReadWrite, Synchronise }
|
||||
|
||||
// Bound by token expiry ApiTokenScope { ReadOnly, ReadWrite, Synchronise }
|
||||
|
||||
UatTokenScope { ReadOnly, // Want to avoid "read write" here to prevent dev confusion.
|
||||
PrivilegeCapable, PrivilegeActive { expiry }, ReadWrite, }
|
||||
|
||||
SessionScope { Ro, RwAlways, PrivCapable, }
|
||||
|
||||
ApiTokenScope { RO RW Sync }
|
||||
|
||||
AuthSession if service account rw always, bound expiry
|
||||
|
||||
if person
|
||||
priv cap, unbound exp
|
||||
- Should we have a "trust the machine flag" to limit exp though?
|
||||
- can we do other types of cryptographic session binding?
|
||||
|
||||
## Session Validation
|
||||
|
||||
- CHANGE: Session with PrivCapable indicates that re-auth can be performed.
|
||||
- CHANGE: Improve how Uat/Api Token scopes become Access Scopes
|
||||
- CHANGE: Remove all AccessScope into other types. ✅
|
||||
|
||||
## Session Re-Authentication
|
||||
|
||||
- Must be performed by the same credential that issued the session originally
|
||||
- This is now stored in the session metadata itself.
|
||||
- Does it need to be in the cred-id?
|
||||
|
||||
- CHANGE: Store the cred id in UAT so that a replica can process the operation in a replication sync
|
||||
failure?
|
||||
- This would rely on re-writing the session.
|
||||
- CHANGE: Should we record in the session when priv-escalations are performed?
|
||||
|
||||
## Misc
|
||||
|
||||
- CHANGE: Compact/shrink UAT size if possible.
|
||||
|
||||
## Diagram
|
||||
|
||||
Set
|
||||
┌───────────────────────PrivActive────────────────────┐
|
||||
│ + Exp │
|
||||
│ │
|
||||
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ .───────────. ┌────────────────┐
|
||||
│ ┌────────────────▶( If Priv Cap )───────▶│Re-Auth-Allowed │
|
||||
│ │ │ │ `───────────' └────────────────┘
|
||||
DB Content ┌ ─ ─ ─ ┼ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
┌───────────────────┐ │ │ JWT │ │ │
|
||||
│ │ │ ▼ │
|
||||
│ AuthSession │ │ ┌──────────────┐ │ ┌──────────────┐ │
|
||||
│ │ │SessionScope │ │ │UatScope │
|
||||
│ Service Account │ │ │- RO │ │ │- RO │ │
|
||||
│ -> RWAlways │──────────────────▶│- RW │─────────┼──▶│- RW │──────────────────────────┐
|
||||
│ │ │ │- PrivCapable │ │ │- PrivCapable │ │ │
|
||||
│ Person │ └──────────────┘ │ │- PrivActive │ │
|
||||
│ -> PrivCap │ │ │ └──────────────┘ │ │
|
||||
│ │ │ │
|
||||
└───────────────────┘ │ │ │ ▼
|
||||
│ ┌──────────────┐
|
||||
│ │ │ │AccessScope │ ┌───────────────┐
|
||||
│ │- RO │ │ │
|
||||
│ │ │ │- RW │───────────▶ │Access Controls│
|
||||
│ │- Sync │ │ │
|
||||
┌───────────────────┐ │ ┌─────────────────┐ │ ┌──────────────┐ │ └──────────────┘ └───────────────┘
|
||||
│ │ │ApiSessionScope │ │ │ApiTokenScope │ ▲
|
||||
│ Create API Token │ │ │- RO │ │ │- RO │ │ │
|
||||
│ │───────────────▶│- RW │────────┼───▶│- RW │─────────────────────────┘
|
||||
│Access Based On Req│ │ │- Sync │ │ │- Sync │ │
|
||||
│ │ └─────────────────┘ │ │ │
|
||||
└───────────────────┘ │ │ └──────────────┘ │
|
||||
│
|
||||
│ │ │
|
||||
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
## TODO:
|
||||
|
||||
1. Remove the ident-only access scope, it's useless! ✅
|
||||
1. Split tokens to have a dedicated session type separate to uat sessions. ✅
|
||||
1. Change uat session access scope recording to match service-account vs person intent.
|
||||
1. Change UAT session issuance to have the uat purpose reflect the readwrite or readwrite-capable
|
||||
nature of the session, based on _auth-type_ that was used.
|
||||
1. Based on auth-type, limit or unlimit expiry to match the intent of the session.
|
|
@ -358,9 +358,9 @@ impl FromStr for UiHint {
|
|||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum UatPurposeStatus {
|
||||
IdentityOnly,
|
||||
ReadOnly,
|
||||
ReadWrite,
|
||||
PrivilegeCapable,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
|
@ -386,9 +386,9 @@ impl fmt::Display for UatStatus {
|
|||
}
|
||||
writeln!(f, "issued_at: {}", self.issued_at)?;
|
||||
match &self.purpose {
|
||||
UatPurposeStatus::IdentityOnly => writeln!(f, "purpose: identity only")?,
|
||||
UatPurposeStatus::ReadOnly => writeln!(f, "purpose: read only")?,
|
||||
UatPurposeStatus::ReadWrite => writeln!(f, "purpose: read write")?,
|
||||
UatPurposeStatus::PrivilegeCapable => writeln!(f, "purpose: privilege capable")?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -397,7 +397,6 @@ impl fmt::Display for UatStatus {
|
|||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum UatPurpose {
|
||||
IdentityOnly,
|
||||
ReadOnly,
|
||||
ReadWrite {
|
||||
/// If none, there is no expiry, and this is always rw. If there is
|
||||
|
@ -446,7 +445,6 @@ impl fmt::Display for UserAuthToken {
|
|||
writeln!(f, "expiry: -")?;
|
||||
}
|
||||
match &self.purpose {
|
||||
UatPurpose::IdentityOnly => writeln!(f, "purpose: identity only")?,
|
||||
UatPurpose::ReadOnly => writeln!(f, "purpose: read only")?,
|
||||
UatPurpose::ReadWrite {
|
||||
expiry: Some(expiry),
|
||||
|
|
|
@ -391,6 +391,8 @@ pub enum DbValueAccessScopeV1 {
|
|||
ReadOnly,
|
||||
#[serde(rename = "w")]
|
||||
ReadWrite,
|
||||
#[serde(rename = "p")]
|
||||
PrivilegeCapable,
|
||||
#[serde(rename = "s")]
|
||||
Synchronise,
|
||||
}
|
||||
|
@ -439,6 +441,35 @@ pub enum DbValueSession {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||
pub enum DbValueApiTokenScopeV1 {
|
||||
#[serde(rename = "r")]
|
||||
#[default]
|
||||
ReadOnly,
|
||||
#[serde(rename = "w")]
|
||||
ReadWrite,
|
||||
#[serde(rename = "s")]
|
||||
Synchronise,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum DbValueApiToken {
|
||||
V1 {
|
||||
#[serde(rename = "u")]
|
||||
refer: Uuid,
|
||||
#[serde(rename = "l")]
|
||||
label: String,
|
||||
#[serde(rename = "e")]
|
||||
expiry: Option<String>,
|
||||
#[serde(rename = "i")]
|
||||
issued_at: String,
|
||||
#[serde(rename = "b")]
|
||||
issued_by: DbValueIdentityId,
|
||||
#[serde(rename = "s", default)]
|
||||
scope: DbValueApiTokenScopeV1,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum DbValueOauth2Session {
|
||||
V1 {
|
||||
|
@ -594,6 +625,8 @@ pub enum DbValueSetV2 {
|
|||
UiHint(Vec<u16>),
|
||||
#[serde(rename = "TO")]
|
||||
TotpSecret(Vec<(String, DbTotpV1)>),
|
||||
#[serde(rename = "AT")]
|
||||
ApiToken(Vec<DbValueApiToken>),
|
||||
}
|
||||
|
||||
impl DbValueSetV2 {
|
||||
|
@ -630,6 +663,7 @@ impl DbValueSetV2 {
|
|||
DbValueSetV2::DeviceKey(set) => set.len(),
|
||||
DbValueSetV2::TrustedDeviceEnrollment(set) => set.len(),
|
||||
DbValueSetV2::Session(set) => set.len(),
|
||||
DbValueSetV2::ApiToken(set) => set.len(),
|
||||
DbValueSetV2::Oauth2Session(set) => set.len(),
|
||||
DbValueSetV2::JwsKeyEs256(set) => set.len(),
|
||||
DbValueSetV2::JwsKeyRs256(set) => set.len(),
|
||||
|
|
|
@ -565,7 +565,7 @@ pub const JSON_SYSTEM_INFO_V1: &str = r#"{
|
|||
"class": ["object", "system_info", "system"],
|
||||
"uuid": ["00000000-0000-0000-0000-ffffff000001"],
|
||||
"description": ["System (local) info and metadata object."],
|
||||
"version": ["11"]
|
||||
"version": ["12"]
|
||||
}
|
||||
}"#;
|
||||
|
||||
|
|
|
@ -1233,7 +1233,7 @@ pub const JSON_SCHEMA_ATTR_API_TOKEN_SESSION: &str = r#"{
|
|||
"api_token_session"
|
||||
],
|
||||
"syntax": [
|
||||
"SESSION"
|
||||
"APITOKEN"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000111"
|
||||
|
@ -1326,7 +1326,7 @@ pub const JSON_SCHEMA_ATTR_SYNC_TOKEN_SESSION: &str = r#"{
|
|||
"sync_token_session"
|
||||
],
|
||||
"syntax": [
|
||||
"SESSION"
|
||||
"APITOKEN"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000115"
|
||||
|
|
|
@ -58,7 +58,7 @@ use crate::repl::entry::EntryChangeState;
|
|||
|
||||
use crate::schema::{SchemaAttribute, SchemaClass, SchemaTransaction};
|
||||
use crate::value::{
|
||||
IndexType, IntentTokenState, Oauth2Session, PartialValue, Session, SyntaxType, Value,
|
||||
ApiToken, IndexType, IntentTokenState, Oauth2Session, PartialValue, Session, SyntaxType, Value,
|
||||
};
|
||||
use crate::valueset::{self, ValueSet};
|
||||
|
||||
|
@ -1961,6 +1961,14 @@ impl<VALID, STATE> Entry<VALID, STATE> {
|
|||
self.attrs.get(attr).and_then(|vs| vs.as_session_map())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get_ava_as_apitoken_map(
|
||||
&self,
|
||||
attr: &str,
|
||||
) -> Option<&std::collections::BTreeMap<Uuid, ApiToken>> {
|
||||
self.attrs.get(attr).and_then(|vs| vs.as_apitoken_map())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get_ava_as_oauth2session_map(
|
||||
&self,
|
||||
|
|
|
@ -209,7 +209,9 @@ impl Account {
|
|||
// TODO: Apply policy to this expiry time.
|
||||
let expiry = expiry_secs
|
||||
.map(|offset| OffsetDateTime::unix_epoch() + ct + Duration::from_secs(offset));
|
||||
|
||||
let issued_at = OffsetDateTime::unix_epoch() + ct;
|
||||
|
||||
// TODO: Apply priv expiry, and what type of token this is (ident, ro, rw).
|
||||
let purpose = UatPurpose::ReadWrite { expiry };
|
||||
|
||||
|
|
|
@ -856,6 +856,10 @@ impl AuthSession {
|
|||
let session_id = Uuid::new_v4();
|
||||
let issue = self.issue;
|
||||
|
||||
// We need to actually work this out better, and then
|
||||
// pass it to to_userauthtoken
|
||||
let scope = SessionScope::ReadWrite;
|
||||
|
||||
security_info!(
|
||||
"Issuing {:?} session {} for {} {}",
|
||||
issue,
|
||||
|
@ -901,7 +905,7 @@ impl AuthSession {
|
|||
expiry: uat.expiry,
|
||||
issued_at: uat.issued_at,
|
||||
issued_by: IdentityId::User(self.account.uuid),
|
||||
scope: (&uat.purpose).into(),
|
||||
scope,
|
||||
}))
|
||||
.map_err(|_| {
|
||||
admin_error!("unable to queue failing authentication as the session will not validate ... ");
|
||||
|
|
|
@ -148,7 +148,7 @@ impl CredentialUpdateSession {
|
|||
}
|
||||
}
|
||||
|
||||
enum MfaRegStateStatus {
|
||||
pub enum MfaRegStateStatus {
|
||||
// Nothing in progress.
|
||||
None,
|
||||
TotpCheck(TotpSecret),
|
||||
|
@ -185,6 +185,16 @@ pub struct CredentialUpdateSessionStatus {
|
|||
mfaregstate: MfaRegStateStatus,
|
||||
}
|
||||
|
||||
impl CredentialUpdateSessionStatus {
|
||||
pub fn can_commit(&self) -> bool {
|
||||
self.can_commit
|
||||
}
|
||||
|
||||
pub fn mfaregstate(&self) -> &MfaRegStateStatus {
|
||||
&self.mfaregstate
|
||||
}
|
||||
}
|
||||
|
||||
// We allow Into here because CUStatus is foreign so it's impossible for us to implement From
|
||||
// in a valid manner
|
||||
#[allow(clippy::from_over_into)]
|
||||
|
@ -2017,7 +2027,7 @@ mod tests {
|
|||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||
let (cust, _) = setup_test_session(idms, ct).await;
|
||||
|
||||
let cutxn = idms.cred_update_transaction();
|
||||
let cutxn = idms.cred_update_transaction_async().await;
|
||||
// The session exists
|
||||
let c_status = cutxn.credential_update_status(&cust, ct);
|
||||
assert!(c_status.is_ok());
|
||||
|
|
|
@ -70,7 +70,7 @@ pub struct AuthSessionRecord {
|
|||
pub expiry: Option<OffsetDateTime>,
|
||||
pub issued_at: OffsetDateTime,
|
||||
pub issued_by: IdentityId,
|
||||
pub scope: AccessScope,
|
||||
pub scope: SessionScope,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -13,6 +13,7 @@ pub mod group;
|
|||
pub mod ldap;
|
||||
pub mod oauth2;
|
||||
pub mod radius;
|
||||
pub mod reauth;
|
||||
pub mod scim;
|
||||
pub mod server;
|
||||
pub mod serviceaccount;
|
||||
|
|
233
kanidmd/lib/src/idm/reauth.rs
Normal file
233
kanidmd/lib/src/idm/reauth.rs
Normal file
|
@ -0,0 +1,233 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
use crate::idm::event::AuthResult;
|
||||
use crate::idm::server::IdmServerAuthTransaction;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ReauthEvent {
|
||||
// pub ident: Option<Identity>,
|
||||
// pub step: AuthEventStep,
|
||||
// pub sessionid: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl<'a> IdmServerAuthTransaction<'a> {
|
||||
pub async fn reauth(
|
||||
&mut self,
|
||||
_ae: &ReauthEvent,
|
||||
_ct: Duration,
|
||||
) -> Result<AuthResult, OperationError> {
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::idm::credupdatesession::{InitCredentialUpdateEvent, MfaRegStateStatus};
|
||||
use crate::idm::delayed::DelayedAction;
|
||||
use crate::idm::event::{AuthEvent, AuthResult};
|
||||
use crate::idm::server::IdmServerTransaction;
|
||||
use crate::idm::AuthState;
|
||||
use crate::prelude::*;
|
||||
|
||||
use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech};
|
||||
|
||||
use uuid::uuid;
|
||||
|
||||
use webauthn_authenticator_rs::softpasskey::SoftPasskey;
|
||||
use webauthn_authenticator_rs::WebauthnAuthenticator;
|
||||
|
||||
const TESTPERSON_UUID: Uuid = uuid!("cf231fea-1a8f-4410-a520-fd9b1a379c86");
|
||||
|
||||
async fn setup_testaccount(idms: &IdmServer, ct: Duration) {
|
||||
let mut idms_prox_write = idms.proxy_write(ct).await;
|
||||
|
||||
let e2 = entry_init!(
|
||||
("class", Value::new_class("object")),
|
||||
("class", Value::new_class("account")),
|
||||
("class", Value::new_class("person")),
|
||||
("name", Value::new_iname("testperson")),
|
||||
("uuid", Value::Uuid(TESTPERSON_UUID)),
|
||||
("description", Value::new_utf8s("testperson")),
|
||||
("displayname", Value::new_utf8s("testperson"))
|
||||
);
|
||||
|
||||
let cr = idms_prox_write.qs_write.internal_create(vec![e2]);
|
||||
assert!(cr.is_ok());
|
||||
assert!(idms_prox_write.commit().is_ok());
|
||||
}
|
||||
|
||||
async fn setup_testaccount_passkey(
|
||||
idms: &IdmServer,
|
||||
ct: Duration,
|
||||
) -> WebauthnAuthenticator<SoftPasskey> {
|
||||
let mut idms_prox_write = idms.proxy_write(ct).await;
|
||||
let testperson = idms_prox_write
|
||||
.qs_write
|
||||
.internal_search_uuid(TESTPERSON_UUID)
|
||||
.expect("failed");
|
||||
let (cust, _c_status) = idms_prox_write
|
||||
.init_credential_update(
|
||||
&InitCredentialUpdateEvent::new_impersonate_entry(testperson),
|
||||
ct,
|
||||
)
|
||||
.expect("Failed to begin credential update.");
|
||||
idms_prox_write.commit().expect("Failed to commit txn");
|
||||
|
||||
// Update session is setup.
|
||||
|
||||
let cutxn = idms.cred_update_transaction();
|
||||
let origin = cutxn.get_origin().clone();
|
||||
|
||||
let mut wa = WebauthnAuthenticator::new(SoftPasskey::new());
|
||||
|
||||
let c_status = cutxn
|
||||
.credential_passkey_init(&cust, ct)
|
||||
.expect("Failed to initiate passkey registration");
|
||||
|
||||
let passkey_chal = match c_status.mfaregstate() {
|
||||
MfaRegStateStatus::Passkey(c) => Some(c),
|
||||
_ => None,
|
||||
}
|
||||
.expect("Unable to access passkey challenge, invalid state");
|
||||
|
||||
let passkey_resp = wa
|
||||
.do_registration(origin.clone(), passkey_chal.clone())
|
||||
.expect("Failed to create soft passkey");
|
||||
|
||||
// Finish the registration
|
||||
let label = "softtoken".to_string();
|
||||
let c_status = cutxn
|
||||
.credential_passkey_finish(&cust, ct, label, &passkey_resp)
|
||||
.expect("Failed to initiate passkey registration");
|
||||
|
||||
assert!(c_status.can_commit());
|
||||
|
||||
drop(cutxn);
|
||||
let mut idms_prox_write = idms.proxy_write(ct).await;
|
||||
|
||||
idms_prox_write
|
||||
.commit_credential_update(&cust, ct)
|
||||
.expect("Failed to commit credential update.");
|
||||
|
||||
idms_prox_write.commit().expect("Failed to commit txn");
|
||||
|
||||
wa
|
||||
}
|
||||
|
||||
async fn auth_passkey(
|
||||
idms: &IdmServer,
|
||||
ct: Duration,
|
||||
wa: &mut WebauthnAuthenticator<SoftPasskey>,
|
||||
idms_delayed: &mut IdmServerDelayed,
|
||||
) -> Option<String> {
|
||||
let mut idms_auth = idms.auth();
|
||||
let origin = idms_auth.get_origin().clone();
|
||||
|
||||
let auth_init = AuthEvent::named_init("testperson");
|
||||
|
||||
let r1 = idms_auth.auth(&auth_init, ct).await;
|
||||
let ar = r1.unwrap();
|
||||
let AuthResult {
|
||||
sessionid,
|
||||
state,
|
||||
delay: _,
|
||||
} = ar;
|
||||
|
||||
if !matches!(state, AuthState::Choose(_)) {
|
||||
debug!("Can't proceed - {:?}", state);
|
||||
return None;
|
||||
};
|
||||
|
||||
let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::Passkey);
|
||||
|
||||
let r2 = idms_auth.auth(&auth_begin, ct).await;
|
||||
let ar = r2.unwrap();
|
||||
let AuthResult {
|
||||
sessionid,
|
||||
state,
|
||||
delay: _,
|
||||
} = ar;
|
||||
|
||||
trace!(?state);
|
||||
|
||||
let rcr = match state {
|
||||
AuthState::Continue(mut allowed) => match allowed.pop() {
|
||||
Some(AuthAllowed::Passkey(rcr)) => rcr,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
trace!(?rcr);
|
||||
|
||||
let resp = wa
|
||||
.do_authentication(origin, rcr)
|
||||
.expect("failed to use softtoken to authenticate");
|
||||
|
||||
let passkey_step = AuthEvent::cred_step_passkey(sessionid, resp);
|
||||
|
||||
let r3 = idms_auth.auth(&passkey_step, ct).await;
|
||||
debug!("r3 ==> {:?}", r3);
|
||||
idms_auth.commit().expect("Must not fail");
|
||||
|
||||
match r3 {
|
||||
Ok(AuthResult {
|
||||
sessionid: _,
|
||||
state: AuthState::Success(token, AuthIssueSession::Token),
|
||||
delay: _,
|
||||
}) => {
|
||||
// Process the webauthn update
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
assert!(matches!(da, DelayedAction::WebauthnCounterIncrement(_)));
|
||||
let r = idms.delayed_action(ct, da).await;
|
||||
assert!(r.is_ok());
|
||||
|
||||
// Process the auth session
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
|
||||
// We have to actually write this one else the following tests
|
||||
// won't work!
|
||||
let r = idms.delayed_action(ct, da).await;
|
||||
assert!(r.is_ok());
|
||||
|
||||
Some(token)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn token_to_ident(idms: &IdmServer, ct: Duration, token: Option<&str>) -> Identity {
|
||||
let mut idms_prox_read = idms.proxy_read().await;
|
||||
|
||||
idms_prox_read
|
||||
.validate_and_parse_token_to_ident(token, ct)
|
||||
.expect("Invalid UAT")
|
||||
}
|
||||
|
||||
#[idm_test]
|
||||
async fn test_idm_reauth_passkey(idms: &IdmServer, idms_delayed: &mut IdmServerDelayed) {
|
||||
let ct = duration_from_epoch_now();
|
||||
|
||||
// Setup the test account
|
||||
setup_testaccount(idms, ct).await;
|
||||
let mut passkey = setup_testaccount_passkey(idms, ct).await;
|
||||
|
||||
// Do an initial auth.
|
||||
let token = auth_passkey(idms, ct, &mut passkey, idms_delayed)
|
||||
.await
|
||||
.expect("failed to authenticate with passkey");
|
||||
|
||||
// Token_str to uat
|
||||
let ident = token_to_ident(idms, ct, Some(token.as_str())).await;
|
||||
|
||||
// Check that the rw entitlement is not present, and that re-auth is allowed.
|
||||
// assert!(matches!(ident.access_scope(), AccessScope::ReadOnly));
|
||||
assert!(matches!(ident.access_scope(), AccessScope::ReadWrite));
|
||||
|
||||
// Assert the session is rw capable though.
|
||||
|
||||
// Do a re-auth
|
||||
|
||||
// They now have the entitlement.
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||
use crate::credential::totp::{Totp, TotpAlgo, TotpDigits};
|
||||
use crate::idm::server::{IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction};
|
||||
use crate::prelude::*;
|
||||
use crate::value::Session;
|
||||
use crate::value::ApiToken;
|
||||
|
||||
use crate::schema::{SchemaClass, SchemaTransaction};
|
||||
|
||||
|
@ -21,7 +21,7 @@ use crate::schema::{SchemaClass, SchemaTransaction};
|
|||
pub(crate) struct SyncAccount {
|
||||
pub name: String,
|
||||
pub uuid: Uuid,
|
||||
pub sync_tokens: BTreeMap<Uuid, Session>,
|
||||
pub sync_tokens: BTreeMap<Uuid, ApiToken>,
|
||||
pub jws_key: JwsSigner,
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,7 @@ macro_rules! try_from_entry {
|
|||
))?;
|
||||
|
||||
let sync_tokens = $value
|
||||
.get_ava_as_session_map("sync_token_session")
|
||||
.get_ava_as_apitoken_map("sync_token_session")
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
|
@ -83,7 +83,7 @@ impl SyncAccount {
|
|||
|
||||
// Get the sessions. There are no gracewindows on sync, we are much stricter.
|
||||
let session_present = entry
|
||||
.get_ava_as_session_map("sync_token_session")
|
||||
.get_ava_as_apitoken_map("sync_token_session")
|
||||
.map(|session_map| session_map.get(&sst.token_id).is_some())
|
||||
.unwrap_or(false);
|
||||
|
||||
|
@ -146,11 +146,12 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
let session_id = Uuid::new_v4();
|
||||
let issued_at = time::OffsetDateTime::unix_epoch() + ct;
|
||||
|
||||
let purpose = ApiTokenPurpose::Synchronise;
|
||||
let scope = ApiTokenScope::Synchronise;
|
||||
let purpose = scope.try_into()?;
|
||||
|
||||
let session = Value::Session(
|
||||
let session = Value::ApiToken(
|
||||
session_id,
|
||||
Session {
|
||||
ApiToken {
|
||||
label: gte.label.clone(),
|
||||
expiry: None,
|
||||
// Need the other inner bits?
|
||||
|
@ -158,11 +159,9 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
issued_at,
|
||||
// Who actually created this?
|
||||
issued_by: gte.ident.get_event_origin_id(),
|
||||
// random id
|
||||
cred_id: Uuid::new_v4(),
|
||||
// What is the access scope of this session? This is
|
||||
// for auditing purposes.
|
||||
scope: (&purpose).into(),
|
||||
scope,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -544,7 +543,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
};
|
||||
|
||||
match sse.ident.access_scope() {
|
||||
AccessScope::IdentityOnly | AccessScope::ReadOnly | AccessScope::ReadWrite => {
|
||||
AccessScope::ReadOnly | AccessScope::ReadWrite => {
|
||||
warn!("Ident access scope is not synchronise");
|
||||
return Err(OperationError::AccessDenied);
|
||||
}
|
||||
|
@ -1347,7 +1346,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
|||
};
|
||||
|
||||
match ident.access_scope() {
|
||||
AccessScope::IdentityOnly | AccessScope::ReadOnly | AccessScope::ReadWrite => {
|
||||
AccessScope::ReadOnly | AccessScope::ReadWrite => {
|
||||
warn!("Ident access scope is not synchronise");
|
||||
return Err(OperationError::AccessDenied);
|
||||
}
|
||||
|
@ -1550,7 +1549,7 @@ mod tests {
|
|||
.expect("Missing attribute: jws_es256_private_key");
|
||||
|
||||
let sync_tokens = sync_entry
|
||||
.get_ava_as_session_map("sync_token_session")
|
||||
.get_ava_as_apitoken_map("sync_token_session")
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
|
|
|
@ -670,7 +670,6 @@ pub trait IdmServerTransaction<'a> {
|
|||
// ✅ Session is valid! Start to setup for it to be used.
|
||||
|
||||
let scope = match uat.purpose {
|
||||
UatPurpose::IdentityOnly => AccessScope::IdentityOnly,
|
||||
UatPurpose::ReadOnly => AccessScope::ReadOnly,
|
||||
UatPurpose::ReadWrite { expiry: None } => AccessScope::ReadWrite,
|
||||
UatPurpose::ReadWrite {
|
||||
|
@ -898,6 +897,11 @@ impl<'a> IdmServerAuthTransaction<'a> {
|
|||
session_read.contains_key(&sessionid)
|
||||
}
|
||||
|
||||
pub fn get_origin(&self) -> &Url {
|
||||
#[allow(clippy::unwrap_used)]
|
||||
self.webauthn.get_allowed_origins().get(0).unwrap()
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip(self))]
|
||||
pub async fn expire_auth_sessions(&mut self, ct: Duration) {
|
||||
// ct is current time - sub the timeout. and then split.
|
||||
|
@ -3692,7 +3696,7 @@ mod tests {
|
|||
expiry: Some(OffsetDateTime::unix_epoch() + expiry_a),
|
||||
issued_at: OffsetDateTime::unix_epoch() + ct,
|
||||
issued_by: IdentityId::User(UUID_ADMIN),
|
||||
scope: AccessScope::IdentityOnly,
|
||||
scope: SessionScope::ReadOnly,
|
||||
});
|
||||
// Persist it.
|
||||
let r = task::block_on(idms.delayed_action(ct, da));
|
||||
|
@ -3727,7 +3731,7 @@ mod tests {
|
|||
expiry: Some(OffsetDateTime::unix_epoch() + expiry_b),
|
||||
issued_at: OffsetDateTime::unix_epoch() + ct,
|
||||
issued_by: IdentityId::User(UUID_ADMIN),
|
||||
scope: AccessScope::IdentityOnly,
|
||||
scope: SessionScope::ReadOnly,
|
||||
});
|
||||
// Persist it.
|
||||
let r = task::block_on(idms.delayed_action(expiry_a, da));
|
||||
|
|
|
@ -2,14 +2,14 @@ use std::collections::BTreeMap;
|
|||
use std::time::Duration;
|
||||
|
||||
use compact_jwt::{Jws, JwsSigner};
|
||||
use kanidm_proto::v1::{ApiToken, ApiTokenPurpose};
|
||||
use kanidm_proto::v1::ApiToken as ProtoApiToken;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::event::SearchEvent;
|
||||
use crate::idm::account::Account;
|
||||
use crate::idm::server::{IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction};
|
||||
use crate::prelude::*;
|
||||
use crate::value::Session;
|
||||
use crate::value::ApiToken;
|
||||
|
||||
// Need to add KID to es256 der for lookups ✅
|
||||
|
||||
|
@ -47,7 +47,7 @@ macro_rules! try_from_entry {
|
|||
))?;
|
||||
|
||||
let api_tokens = $value
|
||||
.get_ava_as_session_map("api_token_session")
|
||||
.get_ava_as_apitoken_map("api_token_session")
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
|
@ -75,7 +75,7 @@ pub struct ServiceAccount {
|
|||
pub valid_from: Option<OffsetDateTime>,
|
||||
pub expire: Option<OffsetDateTime>,
|
||||
|
||||
pub api_tokens: BTreeMap<Uuid, Session>,
|
||||
pub api_tokens: BTreeMap<Uuid, ApiToken>,
|
||||
|
||||
pub jws_key: JwsSigner,
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ impl ServiceAccount {
|
|||
|
||||
pub(crate) fn check_api_token_valid(
|
||||
ct: Duration,
|
||||
apit: &ApiToken,
|
||||
apit: &ProtoApiToken,
|
||||
entry: &Entry<EntrySealed, EntryCommitted>,
|
||||
) -> bool {
|
||||
let within_valid_window = Account::check_within_valid_time(
|
||||
|
@ -108,7 +108,7 @@ impl ServiceAccount {
|
|||
|
||||
// Get the sessions.
|
||||
let session_present = entry
|
||||
.get_ava_as_session_map("api_token_session")
|
||||
.get_ava_as_apitoken_map("api_token_session")
|
||||
.map(|session_map| session_map.get(&apit.token_id).is_some())
|
||||
.unwrap_or(false);
|
||||
|
||||
|
@ -207,16 +207,17 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
// Normalise to UTC in case it was provided as something else.
|
||||
let expiry = gte.expiry.map(|odt| odt.to_offset(time::UtcOffset::UTC));
|
||||
|
||||
let purpose = if gte.read_write {
|
||||
ApiTokenPurpose::ReadWrite
|
||||
let scope = if gte.read_write {
|
||||
ApiTokenScope::ReadWrite
|
||||
} else {
|
||||
ApiTokenPurpose::ReadOnly
|
||||
ApiTokenScope::ReadOnly
|
||||
};
|
||||
let purpose = scope.try_into()?;
|
||||
|
||||
// create a new session
|
||||
let session = Value::Session(
|
||||
let session = Value::ApiToken(
|
||||
session_id,
|
||||
Session {
|
||||
ApiToken {
|
||||
label: gte.label.clone(),
|
||||
expiry,
|
||||
// Need the other inner bits?
|
||||
|
@ -224,16 +225,14 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
issued_at,
|
||||
// Who actually created this?
|
||||
issued_by: gte.ident.get_event_origin_id(),
|
||||
// random id
|
||||
cred_id: Uuid::new_v4(),
|
||||
// What is the access scope of this session? This is
|
||||
// for auditing purposes.
|
||||
scope: (&purpose).into(),
|
||||
scope,
|
||||
},
|
||||
);
|
||||
|
||||
// create the session token (not yet signed)
|
||||
let token = Jws::new(ApiToken {
|
||||
let token = Jws::new(ProtoApiToken {
|
||||
account_id: service_account.uuid,
|
||||
token_id: session_id,
|
||||
label: gte.label.clone(),
|
||||
|
@ -312,7 +311,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
|||
pub fn service_account_list_api_token(
|
||||
&mut self,
|
||||
lte: &ListApiTokenEvent,
|
||||
) -> Result<Vec<ApiToken>, OperationError> {
|
||||
) -> Result<Vec<ProtoApiToken>, OperationError> {
|
||||
// Make an event from the request
|
||||
let srch = match SearchEvent::from_target_uuid_request(
|
||||
lte.ident.clone(),
|
||||
|
@ -334,12 +333,12 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
|||
.and_then(|e| {
|
||||
let account_id = e.get_uuid();
|
||||
// From the entry, turn it into the value
|
||||
e.get_ava_as_session_map("api_token_session").map(|smap| {
|
||||
e.get_ava_as_apitoken_map("api_token_session").map(|smap| {
|
||||
smap.iter()
|
||||
.map(|(u, s)| {
|
||||
s.scope
|
||||
.try_into()
|
||||
.map(|purpose| ApiToken {
|
||||
.map(|purpose| ProtoApiToken {
|
||||
account_id,
|
||||
token_id: *u,
|
||||
label: s.label.clone(),
|
||||
|
|
|
@ -89,7 +89,9 @@ pub mod prelude {
|
|||
QueryServerWriteTransaction,
|
||||
};
|
||||
pub use crate::utils::duration_from_epoch_now;
|
||||
pub use crate::value::{IndexType, PartialValue, SyntaxType, Value};
|
||||
pub use crate::value::{
|
||||
ApiTokenScope, IndexType, PartialValue, SessionScope, SyntaxType, Value,
|
||||
};
|
||||
pub use crate::valueset::{
|
||||
ValueSet, ValueSetBool, ValueSetCid, ValueSetIndex, ValueSetIutf8, ValueSetRefer,
|
||||
ValueSetSecret, ValueSetSpn, ValueSetSyntax, ValueSetT, ValueSetUint32, ValueSetUtf8,
|
||||
|
|
|
@ -834,7 +834,7 @@ mod tests {
|
|||
let pv_parent_id = PartialValue::Refer(parent);
|
||||
let issued_at = curtime_odt;
|
||||
let issued_by = IdentityId::User(tuuid);
|
||||
let scope = AccessScope::IdentityOnly;
|
||||
let scope = SessionScope::ReadOnly;
|
||||
|
||||
// Mod the user
|
||||
let modlist = modlist!([
|
||||
|
|
|
@ -198,7 +198,7 @@ mod tests {
|
|||
let expiry = Some(exp_curtime_odt);
|
||||
let issued_at = curtime_odt;
|
||||
let issued_by = IdentityId::User(tuuid);
|
||||
let scope = AccessScope::IdentityOnly;
|
||||
let scope = SessionScope::ReadOnly;
|
||||
|
||||
let session = Value::Session(
|
||||
session_id,
|
||||
|
@ -316,7 +316,7 @@ mod tests {
|
|||
let expiry = Some(exp_curtime_odt);
|
||||
let issued_at = curtime_odt;
|
||||
let issued_by = IdentityId::User(tuuid);
|
||||
let scope = AccessScope::IdentityOnly;
|
||||
let scope = SessionScope::ReadOnly;
|
||||
|
||||
// Mod the user
|
||||
let modlist = modlist!([
|
||||
|
@ -453,7 +453,7 @@ mod tests {
|
|||
let pv_parent_id = PartialValue::Refer(parent);
|
||||
let issued_at = curtime_odt;
|
||||
let issued_by = IdentityId::User(tuuid);
|
||||
let scope = AccessScope::IdentityOnly;
|
||||
let scope = SessionScope::ReadOnly;
|
||||
|
||||
// Mod the user
|
||||
let modlist = modlist!([
|
||||
|
@ -666,7 +666,7 @@ mod tests {
|
|||
let expiry = None;
|
||||
let issued_at = curtime_odt;
|
||||
let issued_by = IdentityId::User(tuuid);
|
||||
let scope = AccessScope::IdentityOnly;
|
||||
let scope = SessionScope::ReadOnly;
|
||||
|
||||
let session = Value::Session(
|
||||
session_id,
|
||||
|
|
|
@ -216,8 +216,16 @@ pub struct ReplOauth2SessionV1 {
|
|||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Default)]
|
||||
pub enum ReplAccessScopeV1 {
|
||||
IdentityOnly,
|
||||
pub enum ReplSessionScopeV1 {
|
||||
#[default]
|
||||
ReadOnly,
|
||||
ReadWrite,
|
||||
PrivilegeCapable,
|
||||
Synchronise,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Default)]
|
||||
pub enum ReplApiTokenScopeV1 {
|
||||
#[default]
|
||||
ReadOnly,
|
||||
ReadWrite,
|
||||
|
@ -239,7 +247,17 @@ pub struct ReplSessionV1 {
|
|||
pub issued_at: String,
|
||||
pub issued_by: ReplIdentityIdV1,
|
||||
pub cred_id: Uuid,
|
||||
pub scope: ReplAccessScopeV1,
|
||||
pub scope: ReplSessionScopeV1,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
pub struct ReplApiTokenV1 {
|
||||
pub refer: Uuid,
|
||||
pub label: String,
|
||||
pub expiry: Option<String>,
|
||||
pub issued_at: String,
|
||||
pub issued_by: ReplIdentityIdV1,
|
||||
pub scope: ReplApiTokenScopeV1,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
|
@ -344,6 +362,9 @@ pub enum ReplAttrV1 {
|
|||
Session {
|
||||
set: Vec<ReplSessionV1>,
|
||||
},
|
||||
ApiToken {
|
||||
set: Vec<ReplApiTokenV1>,
|
||||
},
|
||||
TotpSecret {
|
||||
set: Vec<(String, ReplTotpV1)>,
|
||||
},
|
||||
|
|
|
@ -205,6 +205,7 @@ impl SchemaAttribute {
|
|||
SyntaxType::DeviceKey => matches!(v, PartialValue::DeviceKey(_)),
|
||||
// Allow refer types.
|
||||
SyntaxType::Session => matches!(v, PartialValue::Refer(_)),
|
||||
SyntaxType::ApiToken => 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(_)),
|
||||
|
@ -255,6 +256,7 @@ impl SchemaAttribute {
|
|||
SyntaxType::Passkey => matches!(v, Value::Passkey(_, _, _)),
|
||||
SyntaxType::DeviceKey => matches!(v, Value::DeviceKey(_, _, _)),
|
||||
SyntaxType::Session => matches!(v, Value::Session(_, _)),
|
||||
SyntaxType::ApiToken => matches!(v, Value::ApiToken(_, _)),
|
||||
SyntaxType::Oauth2Session => matches!(v, Value::Oauth2Session(_, _)),
|
||||
SyntaxType::JwsKeyEs256 => matches!(v, Value::JwsKeyEs256(_)),
|
||||
SyntaxType::JwsKeyRs256 => matches!(v, Value::JwsKeyRs256(_)),
|
||||
|
|
|
@ -66,7 +66,7 @@ fn create_filter_entry<'a>(
|
|||
info!(event = %ident, "Access check for create event");
|
||||
|
||||
match ident.access_scope() {
|
||||
AccessScope::IdentityOnly | AccessScope::ReadOnly | AccessScope::Synchronise => {
|
||||
AccessScope::ReadOnly | AccessScope::Synchronise => {
|
||||
security_access!("denied ❌ - identity access scope is not permitted to create");
|
||||
return IResult::Denied;
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@ fn delete_filter_entry<'a>(
|
|||
info!(event = %ident, "Access check for delete event");
|
||||
|
||||
match ident.access_scope() {
|
||||
AccessScope::IdentityOnly | AccessScope::ReadOnly | AccessScope::Synchronise => {
|
||||
AccessScope::ReadOnly | AccessScope::Synchronise => {
|
||||
security_access!("denied ❌ - identity access scope is not permitted to delete");
|
||||
return IResult::Denied;
|
||||
}
|
||||
|
|
|
@ -1629,17 +1629,9 @@ mod tests {
|
|||
let ev1 = unsafe { E_TESTPERSON_1.clone().into_sealed_committed() };
|
||||
|
||||
let ex_some = vec![Arc::new(ev1.clone())];
|
||||
let ex_none = vec![];
|
||||
|
||||
let r_set = vec![Arc::new(ev1)];
|
||||
|
||||
let se_io = unsafe {
|
||||
SearchEvent::new_impersonate_identity(
|
||||
Identity::from_impersonate_entry_identityonly(E_TEST_ACCOUNT_1.clone()),
|
||||
filter_all!(f_pres("name")),
|
||||
)
|
||||
};
|
||||
|
||||
let se_ro = unsafe {
|
||||
SearchEvent::new_impersonate_identity(
|
||||
Identity::from_impersonate_entry_readonly(E_TEST_ACCOUNT_1.clone()),
|
||||
|
@ -1669,8 +1661,6 @@ mod tests {
|
|||
};
|
||||
|
||||
// Check the admin search event
|
||||
test_acp_search!(&se_io, vec![acp.clone()], r_set.clone(), ex_none);
|
||||
|
||||
test_acp_search!(&se_ro, vec![acp.clone()], r_set.clone(), ex_some);
|
||||
|
||||
test_acp_search!(&se_rw, vec![acp], r_set, ex_some);
|
||||
|
@ -1687,14 +1677,6 @@ mod tests {
|
|||
let exv1 = unsafe { E_TESTPERSON_1_REDUCED.clone().into_sealed_committed() };
|
||||
|
||||
let ex_anon_some = vec![exv1];
|
||||
let ex_anon_none: Vec<EntrySealedCommitted> = vec![];
|
||||
|
||||
let se_anon_io = unsafe {
|
||||
SearchEvent::new_impersonate_identity(
|
||||
Identity::from_impersonate_entry_identityonly(E_TEST_ACCOUNT_1.clone()),
|
||||
filter_all!(f_pres("name")),
|
||||
)
|
||||
};
|
||||
|
||||
let se_anon_ro = unsafe {
|
||||
SearchEvent::new_impersonate_identity(
|
||||
|
@ -1718,8 +1700,6 @@ mod tests {
|
|||
};
|
||||
|
||||
// Finally test it!
|
||||
test_acp_search_reduce!(&se_anon_io, vec![acp.clone()], r_set.clone(), ex_anon_none);
|
||||
|
||||
test_acp_search_reduce!(&se_anon_ro, vec![acp], r_set, ex_anon_some);
|
||||
}
|
||||
|
||||
|
@ -1969,15 +1949,6 @@ mod tests {
|
|||
let ev1 = unsafe { E_TESTPERSON_1.clone().into_sealed_committed() };
|
||||
let r_set = vec![Arc::new(ev1)];
|
||||
|
||||
// Name present
|
||||
let me_pres_io = unsafe {
|
||||
ModifyEvent::new_impersonate_identity(
|
||||
Identity::from_impersonate_entry_identityonly(E_TEST_ACCOUNT_1.clone()),
|
||||
filter_all!(f_eq("name", PartialValue::new_iname("testperson1"))),
|
||||
modlist!([m_pres("name", &Value::new_iname("value"))]),
|
||||
)
|
||||
};
|
||||
|
||||
// Name present
|
||||
let me_pres_ro = unsafe {
|
||||
ModifyEvent::new_impersonate_identity(
|
||||
|
@ -2013,8 +1984,6 @@ mod tests {
|
|||
)
|
||||
};
|
||||
|
||||
test_acp_modify!(&me_pres_io, vec![acp_allow.clone()], &r_set, false);
|
||||
|
||||
test_acp_modify!(&me_pres_ro, vec![acp_allow.clone()], &r_set, false);
|
||||
|
||||
test_acp_modify!(&me_pres_rw, vec![acp_allow], &r_set, true);
|
||||
|
@ -2140,11 +2109,6 @@ mod tests {
|
|||
|
||||
let admin = E_TEST_ACCOUNT_1.clone();
|
||||
|
||||
let ce_admin_io = CreateEvent::new_impersonate_identity(
|
||||
Identity::from_impersonate_entry_identityonly(admin.clone()),
|
||||
vec![],
|
||||
);
|
||||
|
||||
let ce_admin_ro = CreateEvent::new_impersonate_identity(
|
||||
Identity::from_impersonate_entry_readonly(admin.clone()),
|
||||
vec![],
|
||||
|
@ -2171,8 +2135,6 @@ mod tests {
|
|||
)
|
||||
};
|
||||
|
||||
test_acp_create!(&ce_admin_io, vec![acp.clone()], &r1_set, false);
|
||||
|
||||
test_acp_create!(&ce_admin_ro, vec![acp.clone()], &r1_set, false);
|
||||
|
||||
test_acp_create!(&ce_admin_rw, vec![acp], &r1_set, true);
|
||||
|
@ -2244,11 +2206,6 @@ mod tests {
|
|||
|
||||
let admin = E_TEST_ACCOUNT_1.clone();
|
||||
|
||||
let de_admin_io = DeleteEvent::new_impersonate_identity(
|
||||
Identity::from_impersonate_entry_identityonly(admin.clone()),
|
||||
filter_all!(f_eq("name", PartialValue::new_iname("testperson1"))),
|
||||
);
|
||||
|
||||
let de_admin_ro = DeleteEvent::new_impersonate_identity(
|
||||
Identity::from_impersonate_entry_readonly(admin.clone()),
|
||||
filter_all!(f_eq("name", PartialValue::new_iname("testperson1"))),
|
||||
|
@ -2270,8 +2227,6 @@ mod tests {
|
|||
)
|
||||
};
|
||||
|
||||
test_acp_delete!(&de_admin_io, vec![acp.clone()], &r_set, false);
|
||||
|
||||
test_acp_delete!(&de_admin_ro, vec![acp.clone()], &r_set, false);
|
||||
|
||||
test_acp_delete!(&de_admin_rw, vec![acp], &r_set, true);
|
||||
|
|
|
@ -149,7 +149,7 @@ fn modify_ident_test<'a>(ident: &Identity) -> AccessResult<'a> {
|
|||
info!(event = %ident, "Access check for modify event");
|
||||
|
||||
match ident.access_scope() {
|
||||
AccessScope::IdentityOnly | AccessScope::ReadOnly | AccessScope::Synchronise => {
|
||||
AccessScope::ReadOnly | AccessScope::Synchronise => {
|
||||
security_access!("denied ❌ - identity access scope is not permitted to modify");
|
||||
return AccessResult::Denied;
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@ fn search_filter_entry<'a>(
|
|||
info!(event = %ident, "Access check for search (filter) event");
|
||||
|
||||
match ident.access_scope() {
|
||||
AccessScope::IdentityOnly | AccessScope::Synchronise => {
|
||||
AccessScope::Synchronise => {
|
||||
security_access!("denied ❌ - identity access scope is not permitted to search");
|
||||
return AccessResult::Denied;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ use std::hash::Hash;
|
|||
use std::sync::Arc;
|
||||
use uuid::uuid;
|
||||
|
||||
use kanidm_proto::v1::{ApiTokenPurpose, UatPurpose, UatPurposeStatus};
|
||||
use kanidm_proto::v1::{ApiTokenPurpose, UatPurpose};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
@ -17,7 +17,6 @@ use crate::prelude::*;
|
|||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AccessScope {
|
||||
IdentityOnly,
|
||||
ReadOnly,
|
||||
ReadWrite,
|
||||
Synchronise,
|
||||
|
@ -26,7 +25,6 @@ pub enum AccessScope {
|
|||
impl std::fmt::Display for AccessScope {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
AccessScope::IdentityOnly => write!(f, "identity only"),
|
||||
AccessScope::ReadOnly => write!(f, "read only"),
|
||||
AccessScope::ReadWrite => write!(f, "read write"),
|
||||
AccessScope::Synchronise => write!(f, "synchronise"),
|
||||
|
@ -44,42 +42,15 @@ impl From<&ApiTokenPurpose> for AccessScope {
|
|||
}
|
||||
}
|
||||
|
||||
impl TryInto<ApiTokenPurpose> for AccessScope {
|
||||
type Error = OperationError;
|
||||
|
||||
fn try_into(self: AccessScope) -> Result<ApiTokenPurpose, OperationError> {
|
||||
match self {
|
||||
AccessScope::ReadOnly => Ok(ApiTokenPurpose::ReadOnly),
|
||||
AccessScope::ReadWrite => Ok(ApiTokenPurpose::ReadWrite),
|
||||
AccessScope::Synchronise => Ok(ApiTokenPurpose::Synchronise),
|
||||
AccessScope::IdentityOnly => Err(OperationError::InvalidEntryState),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&UatPurpose> for AccessScope {
|
||||
fn from(purpose: &UatPurpose) -> Self {
|
||||
match purpose {
|
||||
UatPurpose::IdentityOnly => AccessScope::IdentityOnly,
|
||||
UatPurpose::ReadOnly => AccessScope::ReadOnly,
|
||||
UatPurpose::ReadWrite { .. } => AccessScope::ReadWrite,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<UatPurposeStatus> for AccessScope {
|
||||
type Error = OperationError;
|
||||
|
||||
fn try_into(self: AccessScope) -> Result<UatPurposeStatus, OperationError> {
|
||||
match self {
|
||||
AccessScope::ReadOnly => Ok(UatPurposeStatus::ReadOnly),
|
||||
AccessScope::ReadWrite => Ok(UatPurposeStatus::ReadWrite),
|
||||
AccessScope::IdentityOnly => Ok(UatPurposeStatus::IdentityOnly),
|
||||
AccessScope::Synchronise => Err(OperationError::InvalidEntryState),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Metadata and the entry of the current Identity which is an external account/user.
|
||||
pub struct IdentUser {
|
||||
|
@ -160,18 +131,6 @@ impl Identity {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn from_impersonate_entry_identityonly(
|
||||
entry: Arc<Entry<EntrySealed, EntryCommitted>>,
|
||||
) -> Self {
|
||||
Identity {
|
||||
origin: IdentType::User(IdentUser { entry }),
|
||||
session_id: uuid!("00000000-0000-0000-0000-000000000000"),
|
||||
scope: AccessScope::IdentityOnly,
|
||||
limits: Limits::unlimited(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn from_impersonate_entry_readonly(entry: Arc<Entry<EntrySealed, EntryCommitted>>) -> Self {
|
||||
Identity {
|
||||
|
|
|
@ -96,6 +96,10 @@ impl QueryServer {
|
|||
if system_info_version < 11 {
|
||||
migrate_txn.migrate_10_to_11()?;
|
||||
}
|
||||
|
||||
if system_info_version < 12 {
|
||||
migrate_txn.migrate_11_to_12()?;
|
||||
}
|
||||
}
|
||||
|
||||
migrate_txn.commit()?;
|
||||
|
@ -332,6 +336,72 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
self.internal_batch_modify(modset.into_iter())
|
||||
}
|
||||
|
||||
/// Migrate 11 to 12
|
||||
///
|
||||
/// Rewrite api-tokens from session to a dedicated api token type.
|
||||
///
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub fn migrate_11_to_12(&mut self) -> Result<(), OperationError> {
|
||||
admin_warn!("starting 11 to 12 migration.");
|
||||
// sync_token_session
|
||||
let filter = filter!(f_or!([
|
||||
f_pres("api_token_session"),
|
||||
f_pres("sync_token_session"),
|
||||
]));
|
||||
|
||||
let mut mod_candidates = self.internal_search_writeable(&filter).map_err(|e| {
|
||||
admin_error!(err = ?e, "migrate_11_to_12 internal search failure");
|
||||
e
|
||||
})?;
|
||||
|
||||
// If there is nothing, we don't need to do anything.
|
||||
if mod_candidates.is_empty() {
|
||||
admin_info!("migrate_11_to_12 no entries to migrate, complete");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// First, filter based on if any credentials present actually are the legacy
|
||||
// webauthn type.
|
||||
|
||||
for (_, ent) in mod_candidates.iter_mut() {
|
||||
if let Some(api_token_session) = ent.pop_ava("api_token_session") {
|
||||
let api_token_session = api_token_session.migrate_session_to_apitoken()
|
||||
.map_err(|e| {
|
||||
error!("Failed to convert api_token_session from session -> apitoken");
|
||||
e
|
||||
})?;
|
||||
|
||||
ent.set_ava_set(
|
||||
"api_token_session",
|
||||
api_token_session);
|
||||
}
|
||||
|
||||
if let Some(sync_token_session) = ent.pop_ava("sync_token_session") {
|
||||
let sync_token_session = sync_token_session.migrate_session_to_apitoken()
|
||||
.map_err(|e| {
|
||||
error!("Failed to convert sync_token_session from session -> apitoken");
|
||||
e
|
||||
})?;
|
||||
|
||||
ent.set_ava_set(
|
||||
"sync_token_session",
|
||||
sync_token_session);
|
||||
}
|
||||
};
|
||||
|
||||
let (
|
||||
pre_candidates,
|
||||
candidates
|
||||
) = mod_candidates
|
||||
.into_iter()
|
||||
.unzip();
|
||||
|
||||
// Apply the batch mod.
|
||||
self.internal_apply_writable(
|
||||
pre_candidates, candidates
|
||||
)
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all)]
|
||||
pub fn initialise_schema_core(&mut self) -> Result<(), OperationError> {
|
||||
admin_debug!("initialise_schema_core -> start ...");
|
||||
|
|
|
@ -497,6 +497,7 @@ pub trait QueryServerTransaction<'a> {
|
|||
SyntaxType::Passkey => Err(OperationError::InvalidAttribute("Passkey Values can not be supplied through modification".to_string())),
|
||||
SyntaxType::DeviceKey => Err(OperationError::InvalidAttribute("DeviceKey Values can not be supplied through modification".to_string())),
|
||||
SyntaxType::Session => Err(OperationError::InvalidAttribute("Session Values can not be supplied through modification".to_string())),
|
||||
SyntaxType::ApiToken => Err(OperationError::InvalidAttribute("ApiToken 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())),
|
||||
|
@ -551,6 +552,7 @@ pub trait QueryServerTransaction<'a> {
|
|||
SyntaxType::ReferenceUuid
|
||||
| SyntaxType::OauthScopeMap
|
||||
| SyntaxType::Session
|
||||
| SyntaxType::ApiToken
|
||||
| SyntaxType::Oauth2Session => {
|
||||
let un = self.name_to_uuid(value).unwrap_or(UUID_DOES_NOT_EXIST);
|
||||
Ok(PartialValue::Refer(un))
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
//! typed values, allows their comparison, filtering and more. It also has the code for serialising
|
||||
//! these into a form for the backend that can be persistent into the [`Backend`](crate::be::Backend).
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
@ -11,7 +13,9 @@ use std::time::Duration;
|
|||
|
||||
use compact_jwt::JwsSigner;
|
||||
use hashbrown::HashSet;
|
||||
use kanidm_proto::v1::ApiTokenPurpose;
|
||||
use kanidm_proto::v1::Filter as ProtoFilter;
|
||||
use kanidm_proto::v1::UatPurposeStatus;
|
||||
use kanidm_proto::v1::UiHint;
|
||||
use num_enum::TryFromPrimitive;
|
||||
use regex::Regex;
|
||||
|
@ -25,7 +29,7 @@ use webauthn_rs::prelude::{DeviceKey as DeviceKeyV4, Passkey as PasskeyV4};
|
|||
use crate::be::dbentry::DbIdentSpn;
|
||||
use crate::credential::{totp::Totp, Credential};
|
||||
use crate::repl::cid::Cid;
|
||||
use crate::server::identity::{AccessScope, IdentityId};
|
||||
use crate::server::identity::IdentityId;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref SPN_RE: Regex = {
|
||||
|
@ -199,6 +203,7 @@ pub enum SyntaxType {
|
|||
Oauth2Session = 28,
|
||||
UiHint = 29,
|
||||
TotpSecret = 30,
|
||||
ApiToken = 31,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for SyntaxType {
|
||||
|
@ -239,6 +244,7 @@ impl TryFrom<&str> for SyntaxType {
|
|||
"OAUTH2SESSION" => Ok(SyntaxType::Oauth2Session),
|
||||
"UIHINT" => Ok(SyntaxType::UiHint),
|
||||
"TOTPSECRET" => Ok(SyntaxType::TotpSecret),
|
||||
"APITOKEN" => Ok(SyntaxType::ApiToken),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
@ -278,6 +284,7 @@ impl fmt::Display for SyntaxType {
|
|||
SyntaxType::Oauth2Session => "OAUTH2SESSION",
|
||||
SyntaxType::UiHint => "UIHINT",
|
||||
SyntaxType::TotpSecret => "TOTPSECRET",
|
||||
SyntaxType::ApiToken => "APITOKEN",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -327,7 +334,6 @@ pub enum PartialValue {
|
|||
Passkey(Uuid),
|
||||
DeviceKey(Uuid),
|
||||
TrustedDeviceEnrollment(Uuid),
|
||||
Session(Uuid),
|
||||
// The label, if any.
|
||||
}
|
||||
|
||||
|
@ -698,7 +704,6 @@ impl PartialValue {
|
|||
PartialValue::PhoneNumber(a) => a.to_string(),
|
||||
PartialValue::IntentToken(u) => u.clone(),
|
||||
PartialValue::TrustedDeviceEnrollment(u) => u.as_hyphenated().to_string(),
|
||||
PartialValue::Session(u) => u.as_hyphenated().to_string(),
|
||||
PartialValue::UiHint(u) => (*u as u16).to_string(),
|
||||
}
|
||||
}
|
||||
|
@ -709,6 +714,56 @@ impl PartialValue {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ApiTokenScope {
|
||||
ReadOnly,
|
||||
ReadWrite,
|
||||
Synchronise,
|
||||
}
|
||||
|
||||
impl TryInto<ApiTokenPurpose> for ApiTokenScope {
|
||||
type Error = OperationError;
|
||||
|
||||
fn try_into(self: ApiTokenScope) -> Result<ApiTokenPurpose, OperationError> {
|
||||
match self {
|
||||
ApiTokenScope::ReadOnly => Ok(ApiTokenPurpose::ReadOnly),
|
||||
ApiTokenScope::ReadWrite => Ok(ApiTokenPurpose::ReadWrite),
|
||||
ApiTokenScope::Synchronise => Ok(ApiTokenPurpose::Synchronise),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ApiToken {
|
||||
pub label: String,
|
||||
pub expiry: Option<OffsetDateTime>,
|
||||
pub issued_at: OffsetDateTime,
|
||||
pub issued_by: IdentityId,
|
||||
pub scope: ApiTokenScope,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SessionScope {
|
||||
ReadOnly,
|
||||
ReadWrite,
|
||||
PrivilegeCapable,
|
||||
// For migration! To be removed in future!
|
||||
Synchronise,
|
||||
}
|
||||
|
||||
impl TryInto<UatPurposeStatus> for SessionScope {
|
||||
type Error = OperationError;
|
||||
|
||||
fn try_into(self: SessionScope) -> Result<UatPurposeStatus, OperationError> {
|
||||
match self {
|
||||
SessionScope::ReadOnly => Ok(UatPurposeStatus::ReadOnly),
|
||||
SessionScope::ReadWrite => Ok(UatPurposeStatus::ReadWrite),
|
||||
SessionScope::PrivilegeCapable => Ok(UatPurposeStatus::PrivilegeCapable),
|
||||
SessionScope::Synchronise => Err(OperationError::InvalidEntryState),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Session {
|
||||
pub label: String,
|
||||
|
@ -716,7 +771,7 @@ pub struct Session {
|
|||
pub issued_at: OffsetDateTime,
|
||||
pub issued_by: IdentityId,
|
||||
pub cred_id: Uuid,
|
||||
pub scope: AccessScope,
|
||||
pub scope: SessionScope,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
@ -771,6 +826,7 @@ pub enum Value {
|
|||
|
||||
TrustedDeviceEnrollment(Uuid),
|
||||
Session(Uuid, Session),
|
||||
ApiToken(Uuid, ApiToken),
|
||||
Oauth2Session(Uuid, Oauth2Session),
|
||||
|
||||
JwsKeyEs256(JwsSigner),
|
||||
|
|
|
@ -16,7 +16,7 @@ use crate::credential::{totp::Totp, Credential};
|
|||
use crate::prelude::*;
|
||||
use crate::repl::{cid::Cid, proto::ReplAttrV1};
|
||||
use crate::schema::SchemaAttribute;
|
||||
use crate::value::{Address, IntentTokenState, Oauth2Session, Session};
|
||||
use crate::value::{Address, ApiToken, IntentTokenState, Oauth2Session, Session};
|
||||
|
||||
mod address;
|
||||
mod binary;
|
||||
|
@ -59,7 +59,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::{ValueSetOauth2Session, ValueSetSession};
|
||||
pub use self::session::{ValueSetApiToken, ValueSetOauth2Session, ValueSetSession};
|
||||
pub use self::spn::ValueSetSpn;
|
||||
pub use self::ssh::ValueSetSshKey;
|
||||
pub use self::syntax::ValueSetSyntax;
|
||||
|
@ -126,6 +126,11 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
|
|||
Ok(None)
|
||||
}
|
||||
|
||||
fn migrate_session_to_apitoken(&self) -> Result<ValueSet, OperationError> {
|
||||
debug_assert!(false);
|
||||
Err(OperationError::InvalidValueState)
|
||||
}
|
||||
|
||||
fn get_ssh_tag(&self, _tag: &str) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
@ -483,6 +488,11 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
|
|||
None
|
||||
}
|
||||
|
||||
fn as_apitoken_map(&self) -> Option<&BTreeMap<Uuid, ApiToken>> {
|
||||
debug_assert!(false);
|
||||
None
|
||||
}
|
||||
|
||||
fn as_oauth2session_map(&self) -> Option<&BTreeMap<Uuid, Oauth2Session>> {
|
||||
debug_assert!(false);
|
||||
None
|
||||
|
@ -575,6 +585,7 @@ pub fn from_result_value_iter(
|
|||
| Value::TotpSecret(_, _)
|
||||
| Value::TrustedDeviceEnrollment(_)
|
||||
| Value::Session(_, _)
|
||||
| Value::ApiToken(_, _)
|
||||
| Value::Oauth2Session(_, _)
|
||||
| Value::JwsKeyEs256(_)
|
||||
| Value::JwsKeyRs256(_) => {
|
||||
|
@ -631,6 +642,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::ApiToken(u, m) => ValueSetApiToken::new(u, m),
|
||||
Value::Oauth2Session(u, m) => ValueSetOauth2Session::new(u, m),
|
||||
Value::UiHint(u) => ValueSetUiHint::new(u),
|
||||
Value::TotpSecret(l, t) => ValueSetTotpSecret::new(l, t),
|
||||
|
@ -677,6 +689,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::ApiToken(set) => ValueSetApiToken::from_dbvs2(set),
|
||||
DbValueSetV2::Oauth2Session(set) => ValueSetOauth2Session::from_dbvs2(set),
|
||||
DbValueSetV2::JwsKeyEs256(set) => ValueSetJwsKeyEs256::from_dbvs2(&set),
|
||||
DbValueSetV2::JwsKeyRs256(set) => ValueSetJwsKeyEs256::from_dbvs2(&set),
|
||||
|
@ -726,6 +739,7 @@ pub fn from_repl_v1(rv1: &ReplAttrV1) -> Result<ValueSet, OperationError> {
|
|||
ReplAttrV1::OauthScopeMap { set } => ValueSetOauthScopeMap::from_repl_v1(set),
|
||||
ReplAttrV1::Oauth2Session { set } => ValueSetOauth2Session::from_repl_v1(set),
|
||||
ReplAttrV1::Session { set } => ValueSetSession::from_repl_v1(set),
|
||||
ReplAttrV1::ApiToken { set } => ValueSetApiToken::from_repl_v1(set),
|
||||
ReplAttrV1::TotpSecret { set } => ValueSetTotpSecret::from_repl_v1(set),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,14 +4,16 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||
use time::OffsetDateTime;
|
||||
|
||||
use crate::be::dbvalue::{
|
||||
DbValueAccessScopeV1, DbValueIdentityId, DbValueOauth2Session, DbValueSession,
|
||||
DbValueAccessScopeV1, DbValueApiToken, DbValueApiTokenScopeV1, DbValueIdentityId,
|
||||
DbValueOauth2Session, DbValueSession,
|
||||
};
|
||||
use crate::prelude::*;
|
||||
use crate::repl::proto::{
|
||||
ReplAccessScopeV1, ReplAttrV1, ReplIdentityIdV1, ReplOauth2SessionV1, ReplSessionV1,
|
||||
ReplApiTokenScopeV1, ReplApiTokenV1, ReplAttrV1, ReplIdentityIdV1, ReplOauth2SessionV1,
|
||||
ReplSessionScopeV1, ReplSessionV1,
|
||||
};
|
||||
use crate::schema::SchemaAttribute;
|
||||
use crate::value::{Oauth2Session, Session};
|
||||
use crate::value::{ApiToken, ApiTokenScope, Oauth2Session, Session, SessionScope};
|
||||
use crate::valueset::{uuid_to_proto_string, DbValueSetV2, ValueSet};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -31,156 +33,162 @@ impl ValueSetSession {
|
|||
}
|
||||
|
||||
pub fn from_dbvs2(data: Vec<DbValueSession>) -> Result<ValueSet, OperationError> {
|
||||
let map = data
|
||||
.into_iter()
|
||||
.filter_map(|dbv| {
|
||||
match dbv {
|
||||
// MISTAKE - Skip due to lack of credential id
|
||||
// Don't actually skip, generate a random cred id. Session cleanup will
|
||||
// trim sessions on users, but if we skip blazenly we invalidate every api
|
||||
// token ever issued. OPPS!
|
||||
DbValueSession::V1 {
|
||||
refer,
|
||||
label,
|
||||
expiry,
|
||||
issued_at,
|
||||
issued_by,
|
||||
scope,
|
||||
} => {
|
||||
let cred_id = Uuid::new_v4();
|
||||
let map =
|
||||
data.into_iter()
|
||||
.filter_map(|dbv| {
|
||||
match dbv {
|
||||
// MISTAKE - Skip due to lack of credential id
|
||||
// Don't actually skip, generate a random cred id. Session cleanup will
|
||||
// trim sessions on users, but if we skip blazenly we invalidate every api
|
||||
// token ever issued. OOPS!
|
||||
DbValueSession::V1 {
|
||||
refer,
|
||||
label,
|
||||
expiry,
|
||||
issued_at,
|
||||
issued_by,
|
||||
scope,
|
||||
} => {
|
||||
let cred_id = Uuid::new_v4();
|
||||
|
||||
// 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!(
|
||||
// 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()?;
|
||||
})
|
||||
.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()?;
|
||||
// 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()?;
|
||||
|
||||
let issued_by = match issued_by {
|
||||
DbValueIdentityId::V1Internal => IdentityId::Internal,
|
||||
DbValueIdentityId::V1Uuid(u) => IdentityId::User(u),
|
||||
DbValueIdentityId::V1Sync(u) => IdentityId::Synch(u),
|
||||
};
|
||||
let issued_by = match issued_by {
|
||||
DbValueIdentityId::V1Internal => IdentityId::Internal,
|
||||
DbValueIdentityId::V1Uuid(u) => IdentityId::User(u),
|
||||
DbValueIdentityId::V1Sync(u) => IdentityId::Synch(u),
|
||||
};
|
||||
|
||||
let scope = match scope {
|
||||
DbValueAccessScopeV1::IdentityOnly => AccessScope::IdentityOnly,
|
||||
DbValueAccessScopeV1::ReadOnly => AccessScope::ReadOnly,
|
||||
DbValueAccessScopeV1::ReadWrite => AccessScope::ReadWrite,
|
||||
DbValueAccessScopeV1::Synchronise => AccessScope::Synchronise,
|
||||
};
|
||||
let scope = match scope {
|
||||
DbValueAccessScopeV1::IdentityOnly
|
||||
| DbValueAccessScopeV1::ReadOnly => SessionScope::ReadOnly,
|
||||
DbValueAccessScopeV1::ReadWrite => SessionScope::ReadWrite,
|
||||
DbValueAccessScopeV1::PrivilegeCapable => {
|
||||
SessionScope::PrivilegeCapable
|
||||
}
|
||||
DbValueAccessScopeV1::Synchronise => SessionScope::Synchronise,
|
||||
};
|
||||
|
||||
Some((
|
||||
Some((
|
||||
refer,
|
||||
Session {
|
||||
label,
|
||||
expiry,
|
||||
issued_at,
|
||||
issued_by,
|
||||
cred_id,
|
||||
scope,
|
||||
},
|
||||
))
|
||||
}
|
||||
DbValueSession::V2 {
|
||||
refer,
|
||||
Session {
|
||||
label,
|
||||
expiry,
|
||||
issued_at,
|
||||
issued_by,
|
||||
cred_id,
|
||||
scope,
|
||||
},
|
||||
))
|
||||
}
|
||||
DbValueSession::V2 {
|
||||
refer,
|
||||
label,
|
||||
expiry,
|
||||
issued_at,
|
||||
issued_by,
|
||||
cred_id,
|
||||
scope,
|
||||
} => {
|
||||
// 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!(
|
||||
label,
|
||||
expiry,
|
||||
issued_at,
|
||||
issued_by,
|
||||
cred_id,
|
||||
scope,
|
||||
} => {
|
||||
// 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()?;
|
||||
})
|
||||
.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()?;
|
||||
// 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()?;
|
||||
|
||||
let issued_by = match issued_by {
|
||||
DbValueIdentityId::V1Internal => IdentityId::Internal,
|
||||
DbValueIdentityId::V1Uuid(u) => IdentityId::User(u),
|
||||
DbValueIdentityId::V1Sync(u) => IdentityId::Synch(u),
|
||||
};
|
||||
let issued_by = match issued_by {
|
||||
DbValueIdentityId::V1Internal => IdentityId::Internal,
|
||||
DbValueIdentityId::V1Uuid(u) => IdentityId::User(u),
|
||||
DbValueIdentityId::V1Sync(u) => IdentityId::Synch(u),
|
||||
};
|
||||
|
||||
let scope = match scope {
|
||||
DbValueAccessScopeV1::IdentityOnly => AccessScope::IdentityOnly,
|
||||
DbValueAccessScopeV1::ReadOnly => AccessScope::ReadOnly,
|
||||
DbValueAccessScopeV1::ReadWrite => AccessScope::ReadWrite,
|
||||
DbValueAccessScopeV1::Synchronise => AccessScope::Synchronise,
|
||||
};
|
||||
let scope = match scope {
|
||||
DbValueAccessScopeV1::IdentityOnly
|
||||
| DbValueAccessScopeV1::ReadOnly => SessionScope::ReadOnly,
|
||||
DbValueAccessScopeV1::ReadWrite => SessionScope::ReadWrite,
|
||||
DbValueAccessScopeV1::PrivilegeCapable => {
|
||||
SessionScope::PrivilegeCapable
|
||||
}
|
||||
DbValueAccessScopeV1::Synchronise => SessionScope::Synchronise,
|
||||
};
|
||||
|
||||
Some((
|
||||
refer,
|
||||
Session {
|
||||
label,
|
||||
expiry,
|
||||
issued_at,
|
||||
issued_by,
|
||||
cred_id,
|
||||
scope,
|
||||
},
|
||||
))
|
||||
Some((
|
||||
refer,
|
||||
Session {
|
||||
label,
|
||||
expiry,
|
||||
issued_at,
|
||||
issued_by,
|
||||
cred_id,
|
||||
scope,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
})
|
||||
.collect();
|
||||
Ok(Box::new(ValueSetSession { map }))
|
||||
}
|
||||
|
||||
|
@ -240,10 +248,10 @@ impl ValueSetSession {
|
|||
};
|
||||
|
||||
let scope = match scope {
|
||||
ReplAccessScopeV1::IdentityOnly => AccessScope::IdentityOnly,
|
||||
ReplAccessScopeV1::ReadOnly => AccessScope::ReadOnly,
|
||||
ReplAccessScopeV1::ReadWrite => AccessScope::ReadWrite,
|
||||
ReplAccessScopeV1::Synchronise => AccessScope::Synchronise,
|
||||
ReplSessionScopeV1::ReadOnly => SessionScope::ReadOnly,
|
||||
ReplSessionScopeV1::ReadWrite => SessionScope::ReadWrite,
|
||||
ReplSessionScopeV1::PrivilegeCapable => SessionScope::PrivilegeCapable,
|
||||
ReplSessionScopeV1::Synchronise => SessionScope::Synchronise,
|
||||
};
|
||||
|
||||
Some((
|
||||
|
@ -365,10 +373,10 @@ impl ValueSetT for ValueSetSession {
|
|||
},
|
||||
cred_id: m.cred_id,
|
||||
scope: match m.scope {
|
||||
AccessScope::IdentityOnly => DbValueAccessScopeV1::IdentityOnly,
|
||||
AccessScope::ReadOnly => DbValueAccessScopeV1::ReadOnly,
|
||||
AccessScope::ReadWrite => DbValueAccessScopeV1::ReadWrite,
|
||||
AccessScope::Synchronise => DbValueAccessScopeV1::Synchronise,
|
||||
SessionScope::ReadOnly => DbValueAccessScopeV1::ReadOnly,
|
||||
SessionScope::ReadWrite => DbValueAccessScopeV1::ReadWrite,
|
||||
SessionScope::PrivilegeCapable => DbValueAccessScopeV1::PrivilegeCapable,
|
||||
SessionScope::Synchronise => DbValueAccessScopeV1::Synchronise,
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
|
@ -398,10 +406,10 @@ impl ValueSetT for ValueSetSession {
|
|||
},
|
||||
cred_id: m.cred_id,
|
||||
scope: match m.scope {
|
||||
AccessScope::IdentityOnly => ReplAccessScopeV1::IdentityOnly,
|
||||
AccessScope::ReadOnly => ReplAccessScopeV1::ReadOnly,
|
||||
AccessScope::ReadWrite => ReplAccessScopeV1::ReadWrite,
|
||||
AccessScope::Synchronise => ReplAccessScopeV1::Synchronise,
|
||||
SessionScope::ReadOnly => ReplSessionScopeV1::ReadOnly,
|
||||
SessionScope::ReadWrite => ReplSessionScopeV1::ReadWrite,
|
||||
SessionScope::PrivilegeCapable => ReplSessionScopeV1::PrivilegeCapable,
|
||||
SessionScope::Synchronise => ReplSessionScopeV1::Synchronise,
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
|
@ -442,6 +450,44 @@ impl ValueSetT for ValueSetSession {
|
|||
// This is what ties us as a type that can be refint checked.
|
||||
Some(Box::new(self.map.keys().copied()))
|
||||
}
|
||||
|
||||
fn migrate_session_to_apitoken(&self) -> Result<ValueSet, OperationError> {
|
||||
let map = self
|
||||
.as_session_map()
|
||||
.iter()
|
||||
.map(|m| m.iter())
|
||||
.flatten()
|
||||
.map(
|
||||
|(
|
||||
u,
|
||||
Session {
|
||||
label,
|
||||
expiry,
|
||||
issued_at,
|
||||
issued_by,
|
||||
cred_id: _,
|
||||
scope,
|
||||
},
|
||||
)| {
|
||||
(
|
||||
*u,
|
||||
ApiToken {
|
||||
label: label.clone(),
|
||||
expiry: expiry.clone(),
|
||||
issued_at: issued_at.clone(),
|
||||
issued_by: issued_by.clone(),
|
||||
scope: match scope {
|
||||
SessionScope::Synchronise => ApiTokenScope::Synchronise,
|
||||
SessionScope::ReadWrite => ApiTokenScope::ReadWrite,
|
||||
_ => ApiTokenScope::ReadOnly,
|
||||
},
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
Ok(Box::new(ValueSetApiToken { map }))
|
||||
}
|
||||
}
|
||||
|
||||
// == oauth2 session ==
|
||||
|
@ -803,3 +849,349 @@ impl ValueSetT for ValueSetOauth2Session {
|
|||
Some(Box::new(self.map.values().map(|m| &m.rs_uuid).copied()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ValueSetApiToken {
|
||||
map: BTreeMap<Uuid, ApiToken>,
|
||||
}
|
||||
|
||||
impl ValueSetApiToken {
|
||||
pub fn new(u: Uuid, m: ApiToken) -> Box<Self> {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(u, m);
|
||||
Box::new(ValueSetApiToken { map })
|
||||
}
|
||||
|
||||
pub fn push(&mut self, u: Uuid, m: ApiToken) -> bool {
|
||||
self.map.insert(u, m).is_none()
|
||||
}
|
||||
|
||||
pub fn from_dbvs2(data: Vec<DbValueApiToken>) -> Result<ValueSet, OperationError> {
|
||||
let map = data
|
||||
.into_iter()
|
||||
.filter_map(|dbv| {
|
||||
match dbv {
|
||||
DbValueApiToken::V1 {
|
||||
refer,
|
||||
label,
|
||||
expiry,
|
||||
issued_at,
|
||||
issued_by,
|
||||
scope,
|
||||
} => {
|
||||
// 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 api token {} 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 api token {} due to invalid expiry timestamp",
|
||||
refer
|
||||
)
|
||||
})
|
||||
// Option<Option<ODT>>
|
||||
.ok()?;
|
||||
|
||||
let issued_by = match issued_by {
|
||||
DbValueIdentityId::V1Internal => IdentityId::Internal,
|
||||
DbValueIdentityId::V1Uuid(u) => IdentityId::User(u),
|
||||
DbValueIdentityId::V1Sync(u) => IdentityId::Synch(u),
|
||||
};
|
||||
|
||||
let scope = match scope {
|
||||
DbValueApiTokenScopeV1::ReadOnly => ApiTokenScope::ReadOnly,
|
||||
DbValueApiTokenScopeV1::ReadWrite => ApiTokenScope::ReadWrite,
|
||||
DbValueApiTokenScopeV1::Synchronise => ApiTokenScope::Synchronise,
|
||||
};
|
||||
|
||||
Some((
|
||||
refer,
|
||||
ApiToken {
|
||||
label,
|
||||
expiry,
|
||||
issued_at,
|
||||
issued_by,
|
||||
scope,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(Box::new(ValueSetApiToken { map }))
|
||||
}
|
||||
|
||||
pub fn from_repl_v1(data: &[ReplApiTokenV1]) -> Result<ValueSet, OperationError> {
|
||||
let map = data
|
||||
.iter()
|
||||
.filter_map(
|
||||
|ReplApiTokenV1 {
|
||||
refer,
|
||||
label,
|
||||
expiry,
|
||||
issued_at,
|
||||
issued_by,
|
||||
scope,
|
||||
}| {
|
||||
// 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
|
||||
.as_ref()
|
||||
.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()?;
|
||||
|
||||
let issued_by = match issued_by {
|
||||
ReplIdentityIdV1::Internal => IdentityId::Internal,
|
||||
ReplIdentityIdV1::Uuid(u) => IdentityId::User(*u),
|
||||
ReplIdentityIdV1::Synch(u) => IdentityId::Synch(*u),
|
||||
};
|
||||
|
||||
let scope = match scope {
|
||||
ReplApiTokenScopeV1::ReadOnly => ApiTokenScope::ReadOnly,
|
||||
ReplApiTokenScopeV1::ReadWrite => ApiTokenScope::ReadWrite,
|
||||
ReplApiTokenScopeV1::Synchronise => ApiTokenScope::Synchronise,
|
||||
};
|
||||
|
||||
Some((
|
||||
*refer,
|
||||
ApiToken {
|
||||
label: label.to_string(),
|
||||
expiry,
|
||||
issued_at,
|
||||
issued_by,
|
||||
scope,
|
||||
},
|
||||
))
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
Ok(Box::new(ValueSetApiToken { map }))
|
||||
}
|
||||
|
||||
// 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, ApiToken)>,
|
||||
{
|
||||
let map = iter.into_iter().collect();
|
||||
Some(Box::new(ValueSetApiToken { map }))
|
||||
}
|
||||
}
|
||||
|
||||
impl ValueSetT for ValueSetApiToken {
|
||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||
match value {
|
||||
Value::ApiToken(u, m) => {
|
||||
if let BTreeEntry::Vacant(e) = self.map.entry(u) {
|
||||
e.insert(m);
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
_ => Err(OperationError::InvalidValueState),
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.map.clear();
|
||||
}
|
||||
|
||||
fn remove(&mut self, pv: &PartialValue) -> bool {
|
||||
match pv {
|
||||
PartialValue::Refer(u) => self.map.remove(u).is_some(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn contains(&self, pv: &PartialValue) -> bool {
|
||||
match pv {
|
||||
PartialValue::Refer(u) => self.map.contains_key(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())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn syntax(&self) -> SyntaxType {
|
||||
SyntaxType::ApiToken
|
||||
}
|
||||
|
||||
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::ApiToken(
|
||||
self.map
|
||||
.iter()
|
||||
.map(|(u, m)| DbValueApiToken::V1 {
|
||||
refer: *u,
|
||||
label: m.label.clone(),
|
||||
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)
|
||||
},
|
||||
issued_by: match m.issued_by {
|
||||
IdentityId::Internal => DbValueIdentityId::V1Internal,
|
||||
IdentityId::User(u) => DbValueIdentityId::V1Uuid(u),
|
||||
IdentityId::Synch(u) => DbValueIdentityId::V1Sync(u),
|
||||
},
|
||||
scope: match m.scope {
|
||||
ApiTokenScope::ReadOnly => DbValueApiTokenScopeV1::ReadOnly,
|
||||
ApiTokenScope::ReadWrite => DbValueApiTokenScopeV1::ReadWrite,
|
||||
ApiTokenScope::Synchronise => DbValueApiTokenScopeV1::Synchronise,
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn to_repl_v1(&self) -> ReplAttrV1 {
|
||||
ReplAttrV1::ApiToken {
|
||||
set: self
|
||||
.map
|
||||
.iter()
|
||||
.map(|(u, m)| ReplApiTokenV1 {
|
||||
refer: *u,
|
||||
label: m.label.clone(),
|
||||
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)
|
||||
},
|
||||
issued_by: match m.issued_by {
|
||||
IdentityId::Internal => ReplIdentityIdV1::Internal,
|
||||
IdentityId::User(u) => ReplIdentityIdV1::Uuid(u),
|
||||
IdentityId::Synch(u) => ReplIdentityIdV1::Synch(u),
|
||||
},
|
||||
scope: match m.scope {
|
||||
ApiTokenScope::ReadOnly => ReplApiTokenScopeV1::ReadOnly,
|
||||
ApiTokenScope::ReadWrite => ReplApiTokenScopeV1::ReadWrite,
|
||||
ApiTokenScope::Synchronise => ReplApiTokenScopeV1::Synchronise,
|
||||
},
|
||||
})
|
||||
.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::ApiToken(*u, m.clone())))
|
||||
}
|
||||
|
||||
fn equal(&self, other: &ValueSet) -> bool {
|
||||
if let Some(other) = other.as_apitoken_map() {
|
||||
&self.map == other
|
||||
} else {
|
||||
debug_assert!(false);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
|
||||
if let Some(b) = other.as_apitoken_map() {
|
||||
mergemaps!(self.map, b)
|
||||
} else {
|
||||
debug_assert!(false);
|
||||
Err(OperationError::InvalidValueState)
|
||||
}
|
||||
}
|
||||
|
||||
fn as_apitoken_map(&self) -> Option<&BTreeMap<Uuid, ApiToken>> {
|
||||
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.
|
||||
Some(Box::new(self.map.keys().copied()))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue