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:
Firstyear 2023-02-24 13:43:19 +10:00 committed by GitHub
parent 6f6189fff8
commit dd761c9a45
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1179 additions and 298 deletions

View 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.

View file

@ -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),

View file

@ -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(),

View file

@ -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"]
}
}"#;

View file

@ -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"

View file

@ -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,

View file

@ -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 };

View file

@ -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 ... ");

View file

@ -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());

View file

@ -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)]

View file

@ -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;

View 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.
}
}

View file

@ -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();

View file

@ -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));

View file

@ -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(),

View file

@ -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,

View file

@ -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!([

View file

@ -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,

View file

@ -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)>,
},

View file

@ -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(_)),

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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 {

View file

@ -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 ...");

View file

@ -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))

View file

@ -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),

View file

@ -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),
}
}

View file

@ -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()))
}
}