diff --git a/kanidm_book/src/accounts_and_groups.md b/kanidm_book/src/accounts_and_groups.md index dcb01ef78..adedb1b9d 100644 --- a/kanidm_book/src/accounts_and_groups.md +++ b/kanidm_book/src/accounts_and_groups.md @@ -191,7 +191,8 @@ kanidm service-account api-token status --name admin ACCOUNT_ID kanidm service-account api-token status --name admin demo_service ``` -To generate a new api token: +By default api tokens are issued to be "read only", so they are unable to make changes on behalf of the +service account they represent. To generate a new read only api token: ```shell kanidm service-account api-token generate --name admin ACCOUNT_ID LABEL [EXPIRY] @@ -199,6 +200,16 @@ kanidm service-account api-token generate --name admin demo_service "Test Token" kanidm service-account api-token generate --name admin demo_service "Test Token" 2020-09-25T11:22:02+10:00 ``` +If you wish to issue a token that is able to make changes on behalf +of the service account, you must add the "--rw" flag during the generate command. It is recommended you +only add --rw when the api-token is performing writes to Kanidm. + +```shell +kanidm service-account api-token generate --name admin ACCOUNT_ID LABEL [EXPIRY] --rw +kanidm service-account api-token generate --name admin demo_service "Test Token" --rw +kanidm service-account api-token generate --name admin demo_service "Test Token" 2020-09-25T11:22:02+10:00 --rw +``` + To destroy (revoke) an api token you will need it's token id. This can be shown with the "status" command. diff --git a/kanidm_client/src/service_account.rs b/kanidm_client/src/service_account.rs index 0a6930113..8a805efd0 100644 --- a/kanidm_client/src/service_account.rs +++ b/kanidm_client/src/service_account.rs @@ -208,10 +208,12 @@ impl KanidmClient { id: &str, label: &str, expiry: Option, + read_write: bool, ) -> Result { let new_token = ApiTokenGenerate { label: label.to_string(), expiry, + read_write, }; self.perform_post_request( format!("/v1/service_account/{}/_api_token", id).as_str(), diff --git a/kanidm_proto/src/v1.rs b/kanidm_proto/src/v1.rs index 29ff9a2d4..efb4482cf 100644 --- a/kanidm_proto/src/v1.rs +++ b/kanidm_proto/src/v1.rs @@ -321,6 +321,17 @@ pub enum UiHint { PosixAccount, } +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "lowercase")] +pub enum UatPurpose { + IdentityOnly, + ReadOnly, + ReadWrite { + #[serde(with = "time::serde::timestamp")] + expiry: time::OffsetDateTime, + }, +} + /// The currently authenticated user, and any required metadata for them /// to properly authorise them. This is similar in nature to oauth and the krb /// PAC/PAD structures. This information is transparent to clients and CAN @@ -338,8 +349,8 @@ pub struct UserAuthToken { // may depend on the client application. #[serde(with = "time::serde::timestamp")] pub expiry: time::OffsetDateTime, + pub purpose: UatPurpose, pub uuid: Uuid, - pub name: String, pub displayname: String, pub spn: String, pub mail_primary: Option, @@ -349,19 +360,21 @@ pub struct UserAuthToken { impl fmt::Display for UserAuthToken { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // writeln!(f, "name: {}", self.name)?; writeln!(f, "spn: {}", self.spn)?; writeln!(f, "uuid: {}", self.uuid)?; writeln!(f, "display: {}", self.displayname)?; + writeln!(f, "expiry: {}", self.expiry)?; + match &self.purpose { + UatPurpose::IdentityOnly => writeln!(f, "purpose: identity only")?, + UatPurpose::ReadOnly => writeln!(f, "purpose: read only")?, + UatPurpose::ReadWrite { expiry } => { + writeln!(f, "purpose: read write (expiry: {})", expiry)? + } + } for group in &self.groups { writeln!(f, "group: {:?}", group.spn)?; } - /* - for claim in &self.claims { - writeln!(f, "claim: {:?}", claim)?; - } - */ - writeln!(f, "token expiry: {}", self.expiry) + Ok(()) } } @@ -373,6 +386,21 @@ impl PartialEq for UserAuthToken { impl Eq for UserAuthToken {} +impl UserAuthToken { + pub fn name(&self) -> &str { + self.spn.split_once('@').map(|x| x.0).unwrap_or(&self.spn) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[serde(rename_all = "lowercase")] +pub enum ApiTokenPurpose { + #[default] + ReadOnly, + ReadWrite, + Synchronise, +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "lowercase")] pub struct ApiToken { @@ -384,6 +412,9 @@ pub struct ApiToken { pub expiry: Option, #[serde(with = "time::serde::timestamp")] pub issued_at: time::OffsetDateTime, + // Defaults to ReadOnly if not present + #[serde(default)] + pub purpose: ApiTokenPurpose, } impl fmt::Display for ApiToken { @@ -422,6 +453,7 @@ pub struct ApiTokenGenerate { pub label: String, #[serde(with = "time::serde::timestamp::option")] pub expiry: Option, + pub read_write: bool, } // UAT will need a downcast to Entry, which adds in the claims to the entry diff --git a/kanidm_tools/src/cli/serviceaccount.rs b/kanidm_tools/src/cli/serviceaccount.rs index af6a9b898..1178b27ee 100644 --- a/kanidm_tools/src/cli/serviceaccount.rs +++ b/kanidm_tools/src/cli/serviceaccount.rs @@ -99,6 +99,7 @@ impl ServiceAccountOpt { copt, label, expiry, + read_write, } => { let expiry_odt = if let Some(t) = expiry { // Convert the time to local timezone. @@ -128,6 +129,7 @@ impl ServiceAccountOpt { aopts.account_id.as_str(), label, expiry_odt, + *read_write, ) .await { diff --git a/kanidm_tools/src/opt/kanidm.rs b/kanidm_tools/src/opt/kanidm.rs index 6320c44c5..4546027f2 100644 --- a/kanidm_tools/src/opt/kanidm.rs +++ b/kanidm_tools/src/opt/kanidm.rs @@ -358,6 +358,8 @@ pub enum ServiceAccountApiToken { /// An optional rfc3339 time of the format "YYYY-MM-DDTHH:MM:SS+TZ", "2020-09-25T11:22:02+10:00". /// After this time the api token will no longer be valid. expiry: Option, + #[clap(long = "rw")] + read_write: bool, }, /// Destroy / revoke an api token from this service account. Access to the /// token is NOT required, only the tag/uuid of the token. diff --git a/kanidmd/core/src/actors/v1_write.rs b/kanidmd/core/src/actors/v1_write.rs index 32a3a0154..3b5f4d654 100644 --- a/kanidmd/core/src/actors/v1_write.rs +++ b/kanidmd/core/src/actors/v1_write.rs @@ -425,6 +425,7 @@ impl QueryServerWriteV1 { uuid_or_name: String, label: String, expiry: Option, + read_write: bool, eventid: Uuid, ) -> Result { let ct = duration_from_epoch_now(); @@ -449,6 +450,7 @@ impl QueryServerWriteV1 { target, label, expiry, + read_write, }; idms_prox_write diff --git a/kanidmd/core/src/https/v1.rs b/kanidmd/core/src/https/v1.rs index ca4669f93..ab6543960 100644 --- a/kanidmd/core/src/https/v1.rs +++ b/kanidmd/core/src/https/v1.rs @@ -455,14 +455,25 @@ pub async fn service_account_api_token_get(req: tide::Request) -> tide pub async fn service_account_api_token_post(mut req: tide::Request) -> tide::Result { let uat = req.get_current_uat(); let uuid_or_name = req.get_url_param("id")?; - let ApiTokenGenerate { label, expiry } = req.body_json().await?; + let ApiTokenGenerate { + label, + expiry, + read_write, + } = req.body_json().await?; let (eventid, hvalue) = req.new_eventid(); let res = req .state() .qe_w_ref - .handle_service_account_api_token_generate(uat, uuid_or_name, label, expiry, eventid) + .handle_service_account_api_token_generate( + uat, + uuid_or_name, + label, + expiry, + read_write, + eventid, + ) .await; to_tide_response(res, hvalue) } @@ -891,7 +902,7 @@ pub async fn group_get_id_unix_token(req: tide::Request) -> tide::Resu } pub async fn domain_get(req: tide::Request) -> tide::Result { - let filter = filter_all!(f_eq("uuid", PartialValue::new_uuid(*UUID_DOMAIN_INFO))); + let filter = filter_all!(f_eq("uuid", PartialValue::new_uuid(UUID_DOMAIN_INFO))); json_rest_event_get(req, filter, None).await } diff --git a/kanidmd/core/tests/proto_v1_test.rs b/kanidmd/core/tests/proto_v1_test.rs index a014b8acf..9545239ec 100644 --- a/kanidmd/core/tests/proto_v1_test.rs +++ b/kanidmd/core/tests/proto_v1_test.rs @@ -1214,7 +1214,7 @@ async fn test_server_api_token_lifecycle() { assert!(tokens.is_empty()); let token = rsclient - .idm_service_account_generate_api_token("test_service", "test token", None) + .idm_service_account_generate_api_token("test_service", "test token", None, false) .await .expect("Failed to create service account api token"); diff --git a/kanidmd/lib/src/access.rs b/kanidmd/lib/src/access.rs index caa8343f2..2e0e83940 100644 --- a/kanidmd/lib/src/access.rs +++ b/kanidmd/lib/src/access.rs @@ -29,13 +29,10 @@ use uuid::Uuid; use crate::entry::{Entry, EntryCommitted, EntryInit, EntryNew, EntryReduced, EntrySealed}; use crate::event::{CreateEvent, DeleteEvent, ModifyEvent, SearchEvent}; use crate::filter::{Filter, FilterValid, FilterValidResolved}; -use crate::identity::{IdentType, IdentityId}; +use crate::identity::{AccessScope, IdentType, IdentityId}; use crate::modify::Modify; use crate::prelude::*; -// const ACP_RELATED_SEARCH_CACHE_MAX: usize = 2048; -// const ACP_RELATED_SEARCH_CACHE_LOCAL: usize = 16; - const ACP_RESOLVE_FILTER_CACHE_MAX: usize = 2048; const ACP_RESOLVE_FILTER_CACHE_LOCAL: usize = 16; @@ -514,7 +511,17 @@ pub trait AccessControlsTransaction<'a> { } IdentType::User(u) => &u.entry, }; - trace!(event = %se.ident, "Access check for search (filter) event"); + info!(event = %se.ident, "Access check for search (filter) event"); + + match se.ident.access_scope() { + AccessScope::IdentityOnly | AccessScope::Synchronise => { + security_access!("denied ❌ - identity access scope is not permitted to search"); + return Ok(vec![]); + } + AccessScope::ReadOnly | AccessScope::ReadWrite => { + // As you were + } + }; // First get the set of acps that apply to this receiver let related_acp: Vec<(&AccessControlSearch, _)> = @@ -616,7 +623,7 @@ pub trait AccessControlsTransaction<'a> { * modify and co. */ - trace!("Access check for search (reduce) event: {}", se.ident); + info!(event = %se.ident, "Access check for search (reduce) event"); // Get the relevant acps for this receiver. let related_acp: Vec<(&AccessControlSearch, _)> = @@ -764,7 +771,17 @@ pub trait AccessControlsTransaction<'a> { } IdentType::User(u) => &u.entry, }; - trace!("Access check for modify event: {}", me.ident); + info!(event = %me.ident, "Access check for modify event"); + + match me.ident.access_scope() { + AccessScope::IdentityOnly | AccessScope::ReadOnly | AccessScope::Synchronise => { + security_access!("denied ❌ - identity access scope is not permitted to modify"); + return Ok(false); + } + AccessScope::ReadWrite => { + // As you were + } + }; // Pre-check if the no-no purge class is present let disallow = me @@ -925,7 +942,17 @@ pub trait AccessControlsTransaction<'a> { } IdentType::User(u) => &u.entry, }; - trace!("Access check for create event: {}", ce.ident); + info!(event = %ce.ident, "Access check for create event"); + + match ce.ident.access_scope() { + AccessScope::IdentityOnly | AccessScope::ReadOnly | AccessScope::Synchronise => { + security_access!("denied ❌ - identity access scope is not permitted to create"); + return Ok(false); + } + AccessScope::ReadWrite => { + // As you were + } + }; // Some useful references we'll use for the remainder of the operation let create_state = self.get_create(); @@ -1056,7 +1083,17 @@ pub trait AccessControlsTransaction<'a> { } IdentType::User(u) => &u.entry, }; - trace!("Access check for delete event: {}", de.ident); + info!(event = %de.ident, "Access check for delete event"); + + match de.ident.access_scope() { + AccessScope::IdentityOnly | AccessScope::ReadOnly | AccessScope::Synchronise => { + security_access!("denied ❌ - identity access scope is not permitted to delete"); + return Ok(false); + } + AccessScope::ReadWrite => { + // As you were + } + }; // Some useful references we'll use for the remainder of the operation let delete_state = self.get_delete(); @@ -1971,6 +2008,40 @@ mod tests { }}; } + macro_rules! test_acp_search_reduce { + ( + $se:expr, + $controls:expr, + $entries:expr, + $expect:expr + ) => {{ + let ac = AccessControls::new(); + let mut acw = ac.write(); + acw.update_search($controls).expect("Failed to update"); + let acw = acw; + + // We still have to reduce the entries to be sure that we are good. + let res = acw + .search_filter_entries(&mut $se, $entries) + .expect("operation failed"); + // Now on the reduced entries, reduce the entries attrs. + let reduced = acw + .search_filter_entry_attributes(&mut $se, res) + .expect("operation failed"); + + // Help the type checker for the expect set. + let expect_set: Vec> = $expect + .into_iter() + .map(|e| unsafe { e.into_reduced() }) + .collect(); + + debug!("expect --> {:?}", expect_set); + debug!("result --> {:?}", reduced); + // should be ok, and same as expect. + assert!(reduced == expect_set); + }}; + } + #[test] fn test_access_internal_search() { // Test that an internal search bypasses ACS @@ -2010,11 +2081,8 @@ mod tests { #[test] fn test_access_enforce_search() { // Test that entries from a search are reduced by acps - let e1: Entry = Entry::unsafe_from_entry_str(JSON_TESTPERSON1); - let ev1 = unsafe { e1.into_sealed_committed() }; - - let e2: Entry = Entry::unsafe_from_entry_str(JSON_TESTPERSON2); - let ev2 = unsafe { e2.into_sealed_committed() }; + let ev1 = unsafe { E_TESTPERSON_1.clone().into_sealed_committed() }; + let ev2 = unsafe { E_TESTPERSON_2.clone().into_sealed_committed() }; let r_set = vec![Arc::new(ev1.clone()), Arc::new(ev2.clone())]; @@ -2049,58 +2117,146 @@ mod tests { test_acp_search!(&se_anon, vec![acp], r_set, ex_anon); } - macro_rules! test_acp_search_reduce { - ( - $se:expr, - $controls:expr, - $entries:expr, - $expect:expr - ) => {{ - let ac = AccessControls::new(); - let mut acw = ac.write(); - acw.update_search($controls).expect("Failed to update"); - let acw = acw; + #[test] + fn test_access_enforce_scope_search() { + let _ = sketching::test_init(); + // Test that identities are bound by their access scope. + let ev1 = unsafe { E_TESTPERSON_1.clone().into_sealed_committed() }; - // We still have to reduce the entries to be sure that we are good. - let res = acw - .search_filter_entries(&mut $se, $entries) - .expect("operation failed"); - // Now on the reduced entries, reduce the entries attrs. - let reduced = acw - .search_filter_entry_attributes(&mut $se, res) - .expect("operation failed"); + let ex_admin_some = vec![Arc::new(ev1.clone())]; + let ex_admin_none = vec![]; - // Help the type checker for the expect set. - let expect_set: Vec> = $expect - .into_iter() - .map(|e| unsafe { e.into_reduced() }) - .collect(); + let r_set = vec![Arc::new(ev1)]; - debug!("expect --> {:?}", expect_set); - debug!("result --> {:?}", reduced); - // should be ok, and same as expect. - assert!(reduced == expect_set); - }}; + let se_admin_io = unsafe { + SearchEvent::new_impersonate_identity( + Identity::from_impersonate_entry_identityonly(Arc::new( + E_ADMIN_V1.clone().into_sealed_committed(), + )), + filter_all!(f_pres("name")), + ) + }; + + let se_admin_ro = unsafe { + SearchEvent::new_impersonate_identity( + Identity::from_impersonate_entry_readonly(Arc::new( + E_ADMIN_V1.clone().into_sealed_committed(), + )), + filter_all!(f_pres("name")), + ) + }; + + let se_admin_rw = unsafe { + SearchEvent::new_impersonate_identity( + Identity::from_impersonate_entry_readwrite(Arc::new( + E_ADMIN_V1.clone().into_sealed_committed(), + )), + filter_all!(f_pres("name")), + ) + }; + + let acp = unsafe { + AccessControlSearch::from_raw( + "test_acp", + "d38640c4-0254-49f9-99b7-8ba7d0233f3d", + // apply to admin only + filter_valid!(f_eq("name", PartialValue::new_iname("admin"))), + // Allow admin to read only testperson1 + filter_valid!(f_eq("name", PartialValue::new_iname("testperson1"))), + // In that read, admin may only view the "name" attribute, or query on + // the name attribute. Any other query (should be) rejected. + "name", + ) + }; + + // Check the admin search event + test_acp_search!( + &se_admin_io, + vec![acp.clone()], + r_set.clone(), + ex_admin_none + ); + + test_acp_search!( + &se_admin_ro, + vec![acp.clone()], + r_set.clone(), + ex_admin_some + ); + + test_acp_search!( + &se_admin_rw, + vec![acp.clone()], + r_set.clone(), + ex_admin_some + ); } - const JSON_TESTPERSON1_REDUCED: &'static str = r#"{ - "attrs": { - "name": ["testperson1"] - } - }"#; + #[test] + fn test_access_enforce_scope_search_attrs() { + // Test that in ident only mode that all attrs are always denied. The op should already have + // "nothing to do" based on search_filter_entries, but we do the "right thing" anyway. + + let ev1 = unsafe { E_TESTPERSON_1.clone().into_sealed_committed() }; + let r_set = vec![Arc::new(ev1.clone())]; + + let exv1 = unsafe { E_TESTPERSON_1_REDUCED.clone().into_sealed_committed() }; + + let ex_anon_some = vec![exv1.clone()]; + let ex_anon_none: Vec = vec![]; + + let se_anon_io = unsafe { + SearchEvent::new_impersonate_identity( + Identity::from_impersonate_entry_identityonly(Arc::new( + E_ANONYMOUS_V1.clone().into_sealed_committed(), + )), + filter_all!(f_pres("name")), + ) + }; + + let se_anon_ro = unsafe { + SearchEvent::new_impersonate_identity( + Identity::from_impersonate_entry_readonly(Arc::new( + E_ANONYMOUS_V1.clone().into_sealed_committed(), + )), + filter_all!(f_pres("name")), + ) + }; + + let acp = unsafe { + AccessControlSearch::from_raw( + "test_acp", + "d38640c4-0254-49f9-99b7-8ba7d0233f3d", + // apply to anonymous only + filter_valid!(f_eq("name", PartialValue::new_iname("anonymous"))), + // Allow anonymous to read only testperson1 + filter_valid!(f_eq("name", PartialValue::new_iname("testperson1"))), + // In that read, admin may only view the "name" attribute, or query on + // the name attribute. Any other query (should be) rejected. + "name", + ) + }; + + // 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); + } + + lazy_static! { + pub static ref E_TESTPERSON_1_REDUCED: EntryInitNew = + entry_init!(("name", Value::new_iname("testperson1"))); + } #[test] fn test_access_enforce_search_attrs() { // Test that attributes are correctly limited. // In this case, we test that a user can only see "name" despite the // class and uuid being present. - let e1: Entry = Entry::unsafe_from_entry_str(JSON_TESTPERSON1); - let ev1 = unsafe { e1.into_sealed_committed() }; + let ev1 = unsafe { E_TESTPERSON_1.clone().into_sealed_committed() }; let r_set = vec![Arc::new(ev1.clone())]; - let ex1: Entry = - Entry::unsafe_from_entry_str(JSON_TESTPERSON1_REDUCED); - let exv1 = unsafe { ex1.into_sealed_committed() }; + let exv1 = unsafe { E_TESTPERSON_1_REDUCED.clone().into_sealed_committed() }; let ex_anon = vec![exv1.clone()]; let se_anon = unsafe { @@ -2133,13 +2289,11 @@ mod tests { // Test that attributes are correctly limited by the request. // In this case, we test that a user can only see "name" despite the // class and uuid being present. - let e1: Entry = Entry::unsafe_from_entry_str(JSON_TESTPERSON1); - let ev1 = unsafe { e1.into_sealed_committed() }; + let ev1 = unsafe { E_TESTPERSON_1.clone().into_sealed_committed() }; + let r_set = vec![Arc::new(ev1.clone())]; - let ex1: Entry = - Entry::unsafe_from_entry_str(JSON_TESTPERSON1_REDUCED); - let exv1 = unsafe { ex1.into_sealed_committed() }; + let exv1 = unsafe { E_TESTPERSON_1_REDUCED.clone().into_sealed_committed() }; let ex_anon = vec![exv1.clone()]; let mut se_anon = unsafe { @@ -2194,8 +2348,7 @@ mod tests { #[test] fn test_access_enforce_modify() { - let e1: Entry = Entry::unsafe_from_entry_str(JSON_TESTPERSON1); - let ev1 = unsafe { e1.into_sealed_committed() }; + let ev1 = unsafe { E_TESTPERSON_1.clone().into_sealed_committed() }; let r_set = vec![Arc::new(ev1.clone())]; // Name present @@ -2331,6 +2484,64 @@ mod tests { test_acp_modify!(&me_rem_class, vec![acp_deny.clone()], &r_set, false); } + #[test] + fn test_access_enforce_scope_modify() { + let ev1 = unsafe { E_TESTPERSON_1.clone().into_sealed_committed() }; + let r_set = vec![Arc::new(ev1.clone())]; + + let admin = Arc::new(unsafe { E_ADMIN_V1.clone().into_sealed_committed() }); + + // Name present + let me_pres_io = unsafe { + ModifyEvent::new_impersonate_identity( + Identity::from_impersonate_entry_identityonly(admin.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( + Identity::from_impersonate_entry_readonly(admin.clone()), + filter_all!(f_eq("name", PartialValue::new_iname("testperson1"))), + modlist!([m_pres("name", &Value::new_iname("value"))]), + ) + }; + + // Name present + let me_pres_rw = unsafe { + ModifyEvent::new_impersonate_identity( + Identity::from_impersonate_entry_readwrite(admin.clone()), + filter_all!(f_eq("name", PartialValue::new_iname("testperson1"))), + modlist!([m_pres("name", &Value::new_iname("value"))]), + ) + }; + + let acp_allow = unsafe { + AccessControlModify::from_raw( + "test_modify_allow", + "87bfe9b8-7600-431e-a492-1dde64bbc455", + // Apply to admin + filter_valid!(f_eq("name", PartialValue::new_iname("admin"))), + // To modify testperson + filter_valid!(f_eq("name", PartialValue::new_iname("testperson1"))), + // Allow pres name and class + "name class", + // Allow rem name and class + "name class", + // And the class allowed is account + "account", + ) + }; + + 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.clone()], &r_set, true); + } + macro_rules! test_acp_create { ( $ce:expr, @@ -2449,6 +2660,51 @@ mod tests { test_acp_create!(&ce_admin, vec![acp, acp2], &r4_set, false); } + #[test] + fn test_access_enforce_scope_create() { + let ev1: Entry = Entry::unsafe_from_entry_str(JSON_TEST_CREATE_AC1); + let r1_set = vec![ev1.clone()]; + + let admin = Arc::new(unsafe { E_ADMIN_V1.clone().into_sealed_committed() }); + + 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![], + ); + + let ce_admin_rw = CreateEvent::new_impersonate_identity( + Identity::from_impersonate_entry_readwrite(admin.clone()), + vec![], + ); + + let acp = unsafe { + AccessControlCreate::from_raw( + "test_create", + "87bfe9b8-7600-431e-a492-1dde64bbc453", + // Apply to admin + filter_valid!(f_eq("name", PartialValue::new_iname("admin"))), + // To create matching filter testperson + // Can this be empty? + filter_valid!(f_eq("name", PartialValue::new_iname("testperson1"))), + // classes + "account", + // attrs + "class name uuid", + ) + }; + + 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); + } + macro_rules! test_acp_delete { ( $de:expr, @@ -2474,8 +2730,7 @@ mod tests { #[test] fn test_access_enforce_delete() { - let e1: Entry = Entry::unsafe_from_entry_str(JSON_TESTPERSON1); - let ev1 = unsafe { e1.into_sealed_committed() }; + let ev1 = unsafe { E_TESTPERSON_1.clone().into_sealed_committed() }; let r_set = vec![Arc::new(ev1.clone())]; let de_admin = unsafe { @@ -2509,6 +2764,46 @@ mod tests { test_acp_delete!(&de_anon, vec![acp], &r_set, false); } + #[test] + fn test_access_enforce_scope_delete() { + let ev1 = unsafe { E_TESTPERSON_1.clone().into_sealed_committed() }; + let r_set = vec![Arc::new(ev1.clone())]; + + let admin = Arc::new(unsafe { E_ADMIN_V1.clone().into_sealed_committed() }); + + 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"))), + ); + + let de_admin_rw = DeleteEvent::new_impersonate_identity( + Identity::from_impersonate_entry_readwrite(admin.clone()), + filter_all!(f_eq("name", PartialValue::new_iname("testperson1"))), + ); + + let acp = unsafe { + AccessControlDelete::from_raw( + "test_delete", + "87bfe9b8-7600-431e-a492-1dde64bbc453", + // Apply to admin + filter_valid!(f_eq("name", PartialValue::new_iname("admin"))), + // To delete testperson + filter_valid!(f_eq("name", PartialValue::new_iname("testperson1"))), + ) + }; + + 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); + } + macro_rules! test_acp_effective_permissions { ( $ident:expr, @@ -2541,7 +2836,11 @@ mod tests { fn test_access_effective_permission_check_1() { let _ = sketching::test_init(); - let admin = unsafe { Identity::from_impersonate_entry_ser(JSON_ADMIN_V1) }; + let admin = unsafe { + Identity::from_impersonate_entry_readwrite(Arc::new( + E_ADMIN_V1.clone().into_sealed_committed(), + )) + }; let e1: Entry = Entry::unsafe_from_entry_str(JSON_TESTPERSON1); let ev1 = unsafe { e1.into_sealed_committed() }; @@ -2579,7 +2878,11 @@ mod tests { fn test_access_effective_permission_check_2() { let _ = sketching::test_init(); - let admin = unsafe { Identity::from_impersonate_entry_ser(JSON_ADMIN_V1) }; + let admin = unsafe { + Identity::from_impersonate_entry_readwrite(Arc::new( + E_ADMIN_V1.clone().into_sealed_committed(), + )) + }; let e1: Entry = Entry::unsafe_from_entry_str(JSON_TESTPERSON1); let ev1 = unsafe { e1.into_sealed_committed() }; diff --git a/kanidmd/lib/src/be/dbvalue.rs b/kanidmd/lib/src/be/dbvalue.rs index cfa92a0ec..4614413ee 100644 --- a/kanidmd/lib/src/be/dbvalue.rs +++ b/kanidmd/lib/src/be/dbvalue.rs @@ -358,6 +358,19 @@ pub struct DbValueOauthScopeMapV1 { pub data: Vec, } +#[derive(Serialize, Deserialize, Debug, Default)] +pub enum DbValueAccessScopeV1 { + #[serde(rename = "i")] + IdentityOnly, + #[serde(rename = "r")] + #[default] + ReadOnly, + #[serde(rename = "w")] + ReadWrite, + #[serde(rename = "s")] + Synchronise, +} + #[derive(Serialize, Deserialize, Debug)] pub enum DbValueIdentityId { #[serde(rename = "v1i")] @@ -379,6 +392,8 @@ pub enum DbValueSession { issued_at: String, #[serde(rename = "b")] issued_by: DbValueIdentityId, + #[serde(rename = "s", default)] + scope: DbValueAccessScopeV1, }, } diff --git a/kanidmd/lib/src/constants/entries.rs b/kanidmd/lib/src/constants/entries.rs index 0b2a259de..dba8adfcb 100644 --- a/kanidmd/lib/src/constants/entries.rs +++ b/kanidmd/lib/src/constants/entries.rs @@ -1,4 +1,11 @@ +use crate::constants::uuids::*; ///! Constant Entries for the IDM +use crate::constants::values::*; +use crate::entry::{Entry, EntryInit, EntryInitNew, EntryNew}; +use crate::value::Value; + +#[cfg(test)] +use uuid::{uuid, Uuid}; /// Builtin System Admin account. pub const JSON_ADMIN_V1: &str = r#"{ @@ -11,6 +18,22 @@ pub const JSON_ADMIN_V1: &str = r#"{ } }"#; +lazy_static! { + pub static ref E_ADMIN_V1: EntryInitNew = entry_init!( + ("class", CLASS_OBJECT.clone()), + ("class", CLASS_MEMBEROF.clone()), + ("class", CLASS_ACCOUNT.clone()), + ("class", CLASS_SERVICE_ACCOUNT.clone()), + ("name", Value::new_iname("admin")), + ("uuid", Value::new_uuid(UUID_ADMIN)), + ( + "description", + Value::new_utf8s("Builtin System Admin account.") + ), + ("displayname", Value::new_utf8s("System Administrator")) + ); +} + /// Builtin IDM Admin account. pub const JSON_IDM_ADMIN_V1: &str = r#"{ "attrs": { @@ -509,7 +532,22 @@ pub const JSON_ANONYMOUS_V1: &str = r#"{ } }"#; +lazy_static! { + pub static ref E_ANONYMOUS_V1: EntryInitNew = entry_init!( + ("class", CLASS_OBJECT.clone()), + ("class", CLASS_ACCOUNT.clone()), + ("class", CLASS_SERVICE_ACCOUNT.clone()), + ("name", Value::new_iname("anonymous")), + ("uuid", Value::new_uuid(UUID_ANONYMOUS)), + ("description", Value::new_utf8s("Anonymous access account.")), + ("displayname", Value::new_utf8s("Anonymous")) + ); +} + // ============ TEST DATA ============ +#[cfg(test)] +pub const UUID_TESTPERSON_1: Uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"); + #[cfg(test)] pub const JSON_TESTPERSON1: &str = r#"{ "attrs": { @@ -519,6 +557,9 @@ pub const JSON_TESTPERSON1: &str = r#"{ } }"#; +#[cfg(test)] +pub const UUID_TESTPERSON_2: Uuid = uuid!("538faac7-4d29-473b-a59d-23023ac19955"); + #[cfg(test)] pub const JSON_TESTPERSON2: &str = r#"{ "attrs": { @@ -527,3 +568,17 @@ pub const JSON_TESTPERSON2: &str = r#"{ "uuid": ["538faac7-4d29-473b-a59d-23023ac19955"] } }"#; + +#[cfg(test)] +lazy_static! { + pub static ref E_TESTPERSON_1: EntryInitNew = entry_init!( + ("class", CLASS_OBJECT.clone()), + ("name", Value::new_iname("testperson1")), + ("uuid", Value::new_uuid(UUID_TESTPERSON_1)) + ); + pub static ref E_TESTPERSON_2: EntryInitNew = entry_init!( + ("class", CLASS_OBJECT.clone()), + ("name", Value::new_iname("testperson2")), + ("uuid", Value::new_uuid(UUID_TESTPERSON_2)) + ); +} diff --git a/kanidmd/lib/src/constants/uuids.rs b/kanidmd/lib/src/constants/uuids.rs index 3cd2e9aa2..284e817de 100644 --- a/kanidmd/lib/src/constants/uuids.rs +++ b/kanidmd/lib/src/constants/uuids.rs @@ -4,6 +4,7 @@ use uuid::{uuid, Uuid}; // Built in group and account ranges. pub const STR_UUID_ADMIN: &str = "00000000-0000-0000-0000-000000000000"; +pub const UUID_ADMIN: Uuid = uuid!("00000000-0000-0000-0000-000000000000"); pub const _UUID_IDM_ADMINS: Uuid = uuid!("00000000-0000-0000-0000-000000000001"); pub const _UUID_IDM_PEOPLE_READ_PRIV: Uuid = uuid!("00000000-0000-0000-0000-000000000002"); pub const _UUID_IDM_PEOPLE_WRITE_PRIV: Uuid = uuid!("00000000-0000-0000-0000-000000000003"); @@ -196,6 +197,7 @@ pub const _UUID_SCHEMA_ATTR_OAUTH2_RS_SUP_SCOPE_MAP: Uuid = // I'd like to strongly criticise william of the past for making poor choices about these allocations. pub const UUID_SYSTEM_INFO: Uuid = uuid!("00000000-0000-0000-0000-ffffff000001"); pub const STR_UUID_DOMAIN_INFO: &str = "00000000-0000-0000-0000-ffffff000025"; +pub const UUID_DOMAIN_INFO: Uuid = uuid!("00000000-0000-0000-0000-ffffff000025"); // DO NOT allocate here, allocate below. @@ -270,8 +272,3 @@ pub const _UUID_IDM_HP_ACP_SERVICE_ACCOUNT_INTO_PERSON_MIGRATE_V1: Uuid = // End of system ranges pub const UUID_DOES_NOT_EXIST: Uuid = uuid!("00000000-0000-0000-0000-fffffffffffe"); pub const UUID_ANONYMOUS: Uuid = uuid!("00000000-0000-0000-0000-ffffffffffff"); - -lazy_static! { - pub static ref UUID_ADMIN: Uuid = Uuid::parse_str(STR_UUID_ADMIN).unwrap(); - pub static ref UUID_DOMAIN_INFO: Uuid = Uuid::parse_str(STR_UUID_DOMAIN_INFO).unwrap(); -} diff --git a/kanidmd/lib/src/constants/values.rs b/kanidmd/lib/src/constants/values.rs index 333da4da0..052257b6f 100644 --- a/kanidmd/lib/src/constants/values.rs +++ b/kanidmd/lib/src/constants/values.rs @@ -34,12 +34,14 @@ lazy_static! { pub static ref PVCLASS_SYSTEM_INFO: PartialValue = PartialValue::new_class("system_info"); pub static ref PVCLASS_SYSTEM_CONFIG: PartialValue = PartialValue::new_class("system_config"); pub static ref PVCLASS_TOMBSTONE: PartialValue = PartialValue::new_class("tombstone"); - pub static ref PVUUID_DOMAIN_INFO: PartialValue = PartialValue::new_uuid(*UUID_DOMAIN_INFO); + pub static ref PVUUID_DOMAIN_INFO: PartialValue = PartialValue::new_uuid(UUID_DOMAIN_INFO); + pub static ref CLASS_ACCOUNT: Value = Value::new_class("account"); pub static ref CLASS_DOMAIN_INFO: Value = Value::new_class("domain_info"); pub static ref CLASS_DYNGROUP: Value = Value::new_class("dyngroup"); pub static ref CLASS_MEMBEROF: Value = Value::new_class("memberof"); pub static ref CLASS_OBJECT: Value = Value::new_class("object"); pub static ref CLASS_RECYCLED: Value = Value::new_class("recycled"); + pub static ref CLASS_SERVICE_ACCOUNT: Value = Value::new_class("service_account"); pub static ref CLASS_SYSTEM: Value = Value::new_class("system"); pub static ref CLASS_SYSTEM_CONFIG: Value = Value::new_class("system_config"); pub static ref CLASS_SYSTEM_INFO: Value = Value::new_class("system_info"); diff --git a/kanidmd/lib/src/entry.rs b/kanidmd/lib/src/entry.rs index f028091e1..626d5ee85 100644 --- a/kanidmd/lib/src/entry.rs +++ b/kanidmd/lib/src/entry.rs @@ -85,8 +85,10 @@ use crate::valueset::{self, ValueSet}; // } // +pub type EntryInitNew = Entry; pub type EntrySealedCommitted = Entry; pub type EntryInvalidCommitted = Entry; +pub type EntryReducedCommitted = Entry; pub type EntryTuple = (Arc, EntryInvalidCommitted); // Entry should have a lifecycle of types. This is Raw (modifiable) and Entry (verified). diff --git a/kanidmd/lib/src/event.rs b/kanidmd/lib/src/event.rs index 743787d84..1300b0b80 100644 --- a/kanidmd/lib/src/event.rs +++ b/kanidmd/lib/src/event.rs @@ -204,8 +204,9 @@ impl SearchEvent { // Just impersonate the account with no filter changes. #[cfg(test)] pub unsafe fn new_impersonate_entry_ser(e: &str, filter: Filter) -> Self { + let ei: Entry = Entry::unsafe_from_entry_str(e); SearchEvent { - ident: Identity::from_impersonate_entry_ser(e), + ident: Identity::from_impersonate_entry_readonly(Arc::new(ei.into_sealed_committed())), filter: filter.clone().into_valid(), filter_orig: filter.into_valid(), attrs: None, @@ -218,7 +219,17 @@ impl SearchEvent { filter: Filter, ) -> Self { SearchEvent { - ident: Identity::from_impersonate_entry(e), + ident: Identity::from_impersonate_entry_readonly(e), + filter: filter.clone().into_valid(), + filter_orig: filter.into_valid(), + attrs: None, + } + } + + #[cfg(test)] + pub unsafe fn new_impersonate_identity(ident: Identity, filter: Filter) -> Self { + SearchEvent { + ident, filter: filter.clone().into_valid(), filter_orig: filter.into_valid(), attrs: None, @@ -247,7 +258,7 @@ impl SearchEvent { let filter_orig = filter.into_valid(); let filter = filter_orig.clone().into_recycled(); SearchEvent { - ident: Identity::from_impersonate_entry(e), + ident: Identity::from_impersonate_entry_readonly(e), filter, filter_orig, attrs: None, @@ -261,7 +272,7 @@ impl SearchEvent { filter: Filter, ) -> Self { SearchEvent { - ident: Identity::from_impersonate_entry(e), + ident: Identity::from_impersonate_entry_readonly(e), filter: filter.clone().into_valid().into_ignore_hidden(), filter_orig: filter.into_valid(), attrs: None, @@ -345,18 +356,26 @@ impl CreateEvent { } } - // Is this an internal only function? #[cfg(test)] pub unsafe fn new_impersonate_entry_ser( e: &str, entries: Vec>, ) -> Self { + let ei: Entry = Entry::unsafe_from_entry_str(e); CreateEvent { - ident: Identity::from_impersonate_entry_ser(e), + ident: Identity::from_impersonate_entry_readwrite(Arc::new(ei.into_sealed_committed())), entries, } } + #[cfg(test)] + pub fn new_impersonate_identity( + ident: Identity, + entries: Vec>, + ) -> Self { + CreateEvent { ident, entries } + } + pub fn new_internal(entries: Vec>) -> Self { CreateEvent { ident: Identity::from_internal(), @@ -447,16 +466,28 @@ impl DeleteEvent { filter: Filter, ) -> Self { DeleteEvent { - ident: Identity::from_impersonate_entry(e), + ident: Identity::from_impersonate_entry_readwrite(e), filter: filter.clone().into_valid(), filter_orig: filter.into_valid(), } } + #[cfg(test)] + pub fn new_impersonate_identity(ident: Identity, filter: Filter) -> Self { + unsafe { + DeleteEvent { + ident, + filter: filter.clone().into_valid(), + filter_orig: filter.into_valid(), + } + } + } + #[cfg(test)] pub unsafe fn new_impersonate_entry_ser(e: &str, filter: Filter) -> Self { + let ei: Entry = Entry::unsafe_from_entry_str(e); DeleteEvent { - ident: Identity::from_impersonate_entry_ser(e), + ident: Identity::from_impersonate_entry_readwrite(Arc::new(ei.into_sealed_committed())), filter: filter.clone().into_valid(), filter_orig: filter.into_valid(), } @@ -618,8 +649,23 @@ impl ModifyEvent { filter: Filter, modlist: ModifyList, ) -> Self { + let ei: Entry = Entry::unsafe_from_entry_str(e); ModifyEvent { - ident: Identity::from_impersonate_entry_ser(e), + ident: Identity::from_impersonate_entry_readwrite(Arc::new(ei.into_sealed_committed())), + filter: filter.clone().into_valid(), + filter_orig: filter.into_valid(), + modlist: modlist.into_valid(), + } + } + + #[cfg(test)] + pub unsafe fn new_impersonate_identity( + ident: Identity, + filter: Filter, + modlist: ModifyList, + ) -> Self { + ModifyEvent { + ident, filter: filter.clone().into_valid(), filter_orig: filter.into_valid(), modlist: modlist.into_valid(), @@ -633,7 +679,7 @@ impl ModifyEvent { modlist: ModifyList, ) -> Self { ModifyEvent { - ident: Identity::from_impersonate_entry(e), + ident: Identity::from_impersonate_entry_readwrite(e), filter: filter.clone().into_valid(), filter_orig: filter.into_valid(), modlist: modlist.into_valid(), @@ -769,7 +815,7 @@ impl ReviveRecycledEvent { filter: Filter, ) -> Self { ReviveRecycledEvent { - ident: Identity::from_impersonate_entry(e), + ident: Identity::from_impersonate_entry_readwrite(e), filter: filter.into_valid(), } } diff --git a/kanidmd/lib/src/identity.rs b/kanidmd/lib/src/identity.rs index 8d9098b97..eb11864ce 100644 --- a/kanidmd/lib/src/identity.rs +++ b/kanidmd/lib/src/identity.rs @@ -7,6 +7,8 @@ use std::collections::BTreeSet; use std::hash::Hash; use std::sync::Arc; +use kanidm_proto::v1::ApiTokenPurpose; + use serde::{Deserialize, Serialize}; use crate::prelude::*; @@ -43,6 +45,48 @@ impl Limits { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AccessScope { + IdentityOnly, + ReadOnly, + ReadWrite, + Synchronise, +} + +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"), + } + } +} + +impl From<&ApiTokenPurpose> for AccessScope { + fn from(purpose: &ApiTokenPurpose) -> Self { + match purpose { + ApiTokenPurpose::ReadOnly => AccessScope::ReadOnly, + ApiTokenPurpose::ReadWrite => AccessScope::ReadWrite, + ApiTokenPurpose::Synchronise => AccessScope::Synchronise, + } + } +} + +impl TryInto for AccessScope { + type Error = OperationError; + + fn try_into(self: AccessScope) -> Result { + match self { + AccessScope::ReadOnly => Ok(ApiTokenPurpose::ReadOnly), + AccessScope::ReadWrite => Ok(ApiTokenPurpose::ReadWrite), + AccessScope::Synchronise => Ok(ApiTokenPurpose::Synchronise), + AccessScope::IdentityOnly => Err(OperationError::InvalidEntryState), + } + } +} + #[derive(Debug, Clone)] /// Metadata and the entry of the current Identity which is an external account/user. pub struct IdentUser { @@ -81,20 +125,24 @@ impl From<&IdentType> for IdentityId { /// An identity that initiated an `Event`. pub struct Identity { pub origin: IdentType, + // pub(crate) source: + // pub(crate) impersonate: bool, + pub(crate) scope: AccessScope, pub(crate) limits: Limits, } impl std::fmt::Display for Identity { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match &self.origin { - IdentType::Internal => write!(f, "Internal"), + IdentType::Internal => write!(f, "Internal ({})", self.scope), IdentType::User(u) => { let nv = u.entry.get_uuid2spn(); write!( f, - "User( {}, {} ) ", + "User( {}, {} ) ({})", nv.to_proto_string_clone(), - u.entry.get_uuid().as_hyphenated() + u.entry.get_uuid().as_hyphenated(), + self.scope ) } } @@ -105,22 +153,44 @@ impl Identity { pub fn from_internal() -> Self { Identity { origin: IdentType::Internal, + scope: AccessScope::ReadWrite, limits: Limits::unlimited(), } } #[cfg(test)] - pub fn from_impersonate_entry(entry: Arc>) -> Self { + pub fn from_impersonate_entry_identityonly( + entry: Arc>, + ) -> Self { Identity { origin: IdentType::User(IdentUser { entry }), + scope: AccessScope::IdentityOnly, limits: Limits::unlimited(), } } #[cfg(test)] - pub unsafe fn from_impersonate_entry_ser(e: &str) -> Self { - let ei: Entry = Entry::unsafe_from_entry_str(e); - Self::from_impersonate_entry(Arc::new(ei.into_sealed_committed())) + pub fn from_impersonate_entry_readonly(entry: Arc>) -> Self { + Identity { + origin: IdentType::User(IdentUser { entry }), + scope: AccessScope::ReadOnly, + limits: Limits::unlimited(), + } + } + + #[cfg(test)] + pub fn from_impersonate_entry_readwrite( + entry: Arc>, + ) -> Self { + Identity { + origin: IdentType::User(IdentUser { entry }), + scope: AccessScope::ReadWrite, + limits: Limits::unlimited(), + } + } + + pub fn access_scope(&self) -> AccessScope { + self.scope } pub fn from_impersonate(ident: &Self) -> Self { diff --git a/kanidmd/lib/src/idm/account.rs b/kanidmd/lib/src/idm/account.rs index c7a76ace7..329880fec 100644 --- a/kanidmd/lib/src/idm/account.rs +++ b/kanidmd/lib/src/idm/account.rs @@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::time::Duration; use kanidm_proto::v1::{ - AuthType, BackupCodesView, CredentialStatus, OperationError, UiHint, UserAuthToken, + AuthType, BackupCodesView, CredentialStatus, OperationError, UatPurpose, UiHint, UserAuthToken, }; use time::OffsetDateTime; use uuid::Uuid; @@ -195,13 +195,17 @@ impl Account { // TODO: Apply policy to this expiry time. let expiry = OffsetDateTime::unix_epoch() + ct + Duration::from_secs(AUTH_SESSION_EXPIRY); + // TODO: Apply priv expiry. + let purpose = UatPurpose::ReadWrite { + expiry: expiry.clone(), + }; Some(UserAuthToken { session_id, auth_type, expiry, + purpose, uuid: self.uuid, - name: self.name.clone(), displayname: self.displayname.clone(), spn: self.spn.clone(), mail_primary: self.mail_primary.clone(), diff --git a/kanidmd/lib/src/idm/credupdatesession.rs b/kanidmd/lib/src/idm/credupdatesession.rs index 1bf68c6a5..3413afa3c 100644 --- a/kanidmd/lib/src/idm/credupdatesession.rs +++ b/kanidmd/lib/src/idm/credupdatesession.rs @@ -234,7 +234,7 @@ impl InitCredentialUpdateIntentEvent { target: Uuid, max_ttl: Duration, ) -> Self { - let ident = Identity::from_impersonate_entry(e); + let ident = Identity::from_impersonate_entry_readwrite(e); InitCredentialUpdateIntentEvent { ident, target, @@ -255,7 +255,7 @@ impl InitCredentialUpdateEvent { #[cfg(test)] pub fn new_impersonate_entry(e: std::sync::Arc>) -> Self { - let ident = Identity::from_impersonate_entry(e); + let ident = Identity::from_impersonate_entry_readwrite(e); let target = ident .get_uuid() .ok_or(OperationError::InvalidState) @@ -278,6 +278,14 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { "Initiating Credential Update Session", ); + // The initiating identity must be in readwrite mode! Effective permission assumes you + // are in rw. + if ident.access_scope() != AccessScope::ReadWrite { + security_access!("identity access scope is not permitted to modify"); + security_access!("denied ❌"); + return Err(OperationError::AccessDenied); + } + // Is target an account? This checks for us. let account = Account::try_from_entry_rw(entry.as_ref(), &mut self.qs_write)?; diff --git a/kanidmd/lib/src/idm/oauth2.rs b/kanidmd/lib/src/idm/oauth2.rs index 9338240dd..4b2984041 100644 --- a/kanidmd/lib/src/idm/oauth2.rs +++ b/kanidmd/lib/src/idm/oauth2.rs @@ -971,6 +971,13 @@ impl Oauth2ResourceServersReadTransaction { // TODO: Can the user consent to which claims are released? Today as we don't support most // of them anyway, no, but in the future, we can stash these to the consent req. + admin_warn!("prefer_short_username: {:?}", o2rs.prefer_short_username); + let preferred_username = if o2rs.prefer_short_username { + Some(code_xchg.uat.name().to_string()) + } else { + Some(code_xchg.uat.spn.clone()) + }; + let (email, email_verified) = if scope_set.contains("email") { if let Some(mp) = code_xchg.uat.mail_primary { (Some(mp), Some(true)) @@ -981,13 +988,6 @@ impl Oauth2ResourceServersReadTransaction { (None, None) }; - admin_warn!("prefer_short_username: {:?}", o2rs.prefer_short_username); - let preferred_username = if o2rs.prefer_short_username { - Some(code_xchg.uat.name.clone()) - } else { - Some(code_xchg.uat.spn.clone()) - }; - // TODO: If max_age was requested in the request, we MUST provide auth_time. // amr == auth method @@ -2427,7 +2427,7 @@ mod tests { == Url::parse("https://idm.example.com/oauth2/openid/test_resource_server") .unwrap() ); - assert!(oidc.sub == OidcSubject::U(*UUID_ADMIN)); + assert!(oidc.sub == OidcSubject::U(UUID_ADMIN)); assert!(oidc.aud == "test_resource_server"); assert!(oidc.iat == iat); assert!(oidc.nbf == Some(iat)); @@ -2600,7 +2600,7 @@ mod tests { .validate(&jws_validator, iat) .expect("Failed to verify oidc"); - assert!(oidc.sub == OidcSubject::U(*UUID_ADMIN)); + assert!(oidc.sub == OidcSubject::U(UUID_ADMIN)); } ) } diff --git a/kanidmd/lib/src/idm/server.rs b/kanidmd/lib/src/idm/server.rs index 76811186f..ef12f7d79 100644 --- a/kanidmd/lib/src/idm/server.rs +++ b/kanidmd/lib/src/idm/server.rs @@ -15,8 +15,8 @@ use fernet::Fernet; use futures::task as futures_task; use hashbrown::HashSet; use kanidm_proto::v1::{ - ApiToken, BackupCodesView, CredentialStatus, PasswordFeedback, RadiusAuthToken, UnixGroupToken, - UnixUserToken, UserAuthToken, + ApiToken, BackupCodesView, CredentialStatus, PasswordFeedback, RadiusAuthToken, UatPurpose, + UnixGroupToken, UnixUserToken, UserAuthToken, }; use rand::prelude::*; use tokio::sync::mpsc::{ @@ -31,7 +31,7 @@ use super::delayed::BackupCodeRemoval; use super::event::ReadBackupCodeEvent; use crate::credential::policy::CryptoPolicy; use crate::credential::softlock::CredSoftLock; -use crate::identity::{IdentType, IdentUser, Limits}; +use crate::identity::{AccessScope, IdentType, IdentUser, Limits}; use crate::idm::account::Account; use crate::idm::authsession::AuthSession; use crate::idm::credupdatesession::CredentialUpdateSessionMutex; @@ -610,6 +610,19 @@ pub trait IdmServerTransaction<'a> { return Err(OperationError::SessionExpired); } + let scope = match uat.purpose { + UatPurpose::IdentityOnly => AccessScope::IdentityOnly, + UatPurpose::ReadOnly => AccessScope::ReadOnly, + UatPurpose::ReadWrite { expiry } => { + let cot = time::OffsetDateTime::unix_epoch() + ct; + if cot < expiry { + AccessScope::ReadWrite + } else { + AccessScope::ReadOnly + } + } + }; + // #64: Now apply claims from the uat into the Entry // to allow filtering. /* @@ -644,6 +657,7 @@ pub trait IdmServerTransaction<'a> { let limits = Limits::default(); Ok(Identity { origin: IdentType::User(IdentUser { entry }), + scope, limits, }) } @@ -662,9 +676,12 @@ pub trait IdmServerTransaction<'a> { return Err(OperationError::SessionExpired); } + let scope = (&apit.purpose).into(); + let limits = Limits::default(); Ok(Identity { origin: IdentType::User(IdentUser { entry }), + scope, limits, }) } @@ -703,6 +720,7 @@ pub trait IdmServerTransaction<'a> { let limits = Limits::default(); Ok(Identity { origin: IdentType::User(IdentUser { entry: anon_entry }), + scope: AccessScope::ReadOnly, limits, }) } else { @@ -3588,7 +3606,7 @@ mod tests { let idms_prox_write = idms.proxy_write(ct.clone()); let me_reset_tokens = unsafe { ModifyEvent::new_internal_invalid( - filter!(f_eq("uuid", PartialValue::new_uuid(*UUID_DOMAIN_INFO))), + filter!(f_eq("uuid", PartialValue::new_uuid(UUID_DOMAIN_INFO))), ModifyList::new_list(vec![ Modify::Purged(AttrString::from("fernet_private_key_str")), Modify::Purged(AttrString::from("es256_private_key_der")), diff --git a/kanidmd/lib/src/idm/serviceaccount.rs b/kanidmd/lib/src/idm/serviceaccount.rs index 377e34499..eca767bd7 100644 --- a/kanidmd/lib/src/idm/serviceaccount.rs +++ b/kanidmd/lib/src/idm/serviceaccount.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use std::time::Duration; use compact_jwt::{Jws, JwsSigner}; -use kanidm_proto::v1::ApiToken; +use kanidm_proto::v1::{ApiToken, ApiTokenPurpose}; use time::OffsetDateTime; use crate::event::SearchEvent; @@ -150,6 +150,8 @@ pub struct GenerateApiTokenEvent { pub label: String, // When should it expire? pub expiry: Option, + // Is it read_write capable? + pub read_write: bool, // Limits? } @@ -161,6 +163,7 @@ impl GenerateApiTokenEvent { target, label: label.to_string(), expiry: expiry.map(|ct| time::OffsetDateTime::unix_epoch() + ct), + read_write: false, } } } @@ -209,6 +212,12 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { .clone() .map(|odt| odt.to_offset(time::UtcOffset::UTC)); + let purpose = if gte.read_write { + ApiTokenPurpose::ReadWrite + } else { + ApiTokenPurpose::ReadOnly + }; + // create a new session let session = Value::Session( session_id, @@ -220,6 +229,9 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { issued_at, // Who actually created this? issued_by: gte.ident.get_event_origin_id(), + // What is the access scope of this session? This is + // for auditing purposes. + scope: (&purpose).into(), }, ); @@ -230,6 +242,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { label: gte.label.clone(), expiry: gte.expiry.clone(), issued_at, + purpose, }); // modify the account to put the session onto it. @@ -318,7 +331,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> { match self.qs_read.search_ext(&srch) { Ok(mut entries) => { - let r = entries + entries .pop() // get the first entry .and_then(|e| { @@ -326,21 +339,29 @@ impl<'a> IdmServerProxyReadTransaction<'a> { // From the entry, turn it into the value e.get_ava_as_session_map("api_token_session").map(|smap| { smap.iter() - .map(|(u, s)| ApiToken { - account_id, - token_id: *u, - label: s.label.clone(), - expiry: s.expiry.clone(), - issued_at: s.issued_at.clone(), + .map(|(u, s)| { + s.scope + .try_into() + .map(|purpose| ApiToken { + account_id, + token_id: *u, + label: s.label.clone(), + expiry: s.expiry.clone(), + issued_at: s.issued_at.clone(), + purpose, + }) + .map_err(|e| { + admin_error!("Invalid api_token {}", u); + e + }) }) - .collect::>() + .collect::, _>>() }) }) .unwrap_or_else(|| { // No matching entry? Return none. - Vec::new() - }); - Ok(r) + Ok(Vec::new()) + }) } Err(e) => Err(e), } diff --git a/kanidmd/lib/src/ldap.rs b/kanidmd/lib/src/ldap.rs index 843977673..927fce081 100644 --- a/kanidmd/lib/src/ldap.rs +++ b/kanidmd/lib/src/ldap.rs @@ -598,16 +598,16 @@ mod tests { let admin_t = task::block_on(ldaps.do_bind(idms, "admin", TEST_PASSWORD)) .unwrap() .unwrap(); - assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN)); + assert!(admin_t.effective_session == LdapSession::UnixBind(UUID_ADMIN)); let admin_t = task::block_on(ldaps.do_bind(idms, "admin@example.com", TEST_PASSWORD)) .unwrap() .unwrap(); - assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN)); + assert!(admin_t.effective_session == LdapSession::UnixBind(UUID_ADMIN)); let admin_t = task::block_on(ldaps.do_bind(idms, STR_UUID_ADMIN, TEST_PASSWORD)) .unwrap() .unwrap(); - assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN)); + assert!(admin_t.effective_session == LdapSession::UnixBind(UUID_ADMIN)); let admin_t = task::block_on(ldaps.do_bind( idms, "name=admin,dc=example,dc=com", @@ -615,7 +615,7 @@ mod tests { )) .unwrap() .unwrap(); - assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN)); + assert!(admin_t.effective_session == LdapSession::UnixBind(UUID_ADMIN)); let admin_t = task::block_on(ldaps.do_bind( idms, "spn=admin@example.com,dc=example,dc=com", @@ -623,7 +623,7 @@ mod tests { )) .unwrap() .unwrap(); - assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN)); + assert!(admin_t.effective_session == LdapSession::UnixBind(UUID_ADMIN)); let admin_t = task::block_on(ldaps.do_bind( idms, format!("uuid={},dc=example,dc=com", STR_UUID_ADMIN).as_str(), @@ -631,17 +631,17 @@ mod tests { )) .unwrap() .unwrap(); - assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN)); + assert!(admin_t.effective_session == LdapSession::UnixBind(UUID_ADMIN)); let admin_t = task::block_on(ldaps.do_bind(idms, "name=admin", TEST_PASSWORD)) .unwrap() .unwrap(); - assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN)); + assert!(admin_t.effective_session == LdapSession::UnixBind(UUID_ADMIN)); let admin_t = task::block_on(ldaps.do_bind(idms, "spn=admin@example.com", TEST_PASSWORD)) .unwrap() .unwrap(); - assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN)); + assert!(admin_t.effective_session == LdapSession::UnixBind(UUID_ADMIN)); let admin_t = task::block_on(ldaps.do_bind( idms, format!("uuid={}", STR_UUID_ADMIN).as_str(), @@ -649,13 +649,13 @@ mod tests { )) .unwrap() .unwrap(); - assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN)); + assert!(admin_t.effective_session == LdapSession::UnixBind(UUID_ADMIN)); let admin_t = task::block_on(ldaps.do_bind(idms, "admin,dc=example,dc=com", TEST_PASSWORD)) .unwrap() .unwrap(); - assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN)); + assert!(admin_t.effective_session == LdapSession::UnixBind(UUID_ADMIN)); let admin_t = task::block_on(ldaps.do_bind( idms, "admin@example.com,dc=example,dc=com", @@ -663,7 +663,7 @@ mod tests { )) .unwrap() .unwrap(); - assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN)); + assert!(admin_t.effective_session == LdapSession::UnixBind(UUID_ADMIN)); let admin_t = task::block_on(ldaps.do_bind( idms, format!("{},dc=example,dc=com", STR_UUID_ADMIN).as_str(), @@ -671,7 +671,7 @@ mod tests { )) .unwrap() .unwrap(); - assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN)); + assert!(admin_t.effective_session == LdapSession::UnixBind(UUID_ADMIN)); // Bad password, check last to prevent softlocking of the admin account. assert!(task::block_on(ldaps.do_bind(idms, "admin", "test")) diff --git a/kanidmd/lib/src/lib.rs b/kanidmd/lib/src/lib.rs index ef9deb8f5..221c18d25 100644 --- a/kanidmd/lib/src/lib.rs +++ b/kanidmd/lib/src/lib.rs @@ -68,14 +68,15 @@ pub mod prelude { pub use crate::constants::*; pub use crate::entry::{ - Entry, EntryCommitted, EntryInit, EntryInvalid, EntryInvalidCommitted, EntryNew, - EntryReduced, EntrySealed, EntrySealedCommitted, EntryTuple, EntryValid, + Entry, EntryCommitted, EntryInit, EntryInitNew, EntryInvalid, EntryInvalidCommitted, + EntryNew, EntryReduced, EntryReducedCommitted, EntrySealed, EntrySealedCommitted, + EntryTuple, EntryValid, }; pub use crate::filter::{ f_and, f_andnot, f_eq, f_id, f_inc, f_lt, f_or, f_pres, f_self, f_spn_name, f_sub, Filter, FilterInvalid, FC, }; - pub use crate::identity::Identity; + pub use crate::identity::{AccessScope, Identity}; pub use crate::modify::{m_pres, m_purge, m_remove, Modify, ModifyInvalid, ModifyList}; pub use crate::server::{ QueryServer, QueryServerReadTransaction, QueryServerTransaction, diff --git a/kanidmd/lib/src/plugins/base.rs b/kanidmd/lib/src/plugins/base.rs index 6d351b71d..bffca82c7 100644 --- a/kanidmd/lib/src/plugins/base.rs +++ b/kanidmd/lib/src/plugins/base.rs @@ -84,11 +84,6 @@ impl Plugin for Base { } } - // Setup UUIDS because lazy_static can't create a type valid for range. - let uuid_admin = *UUID_ADMIN; - let uuid_anonymous = UUID_ANONYMOUS; - let uuid_does_not_exist = UUID_DOES_NOT_EXIST; - // Check that the system-protected range is not in the cand_uuid, unless we are // an internal operation. if !ce.ident.is_internal() { @@ -97,7 +92,7 @@ impl Plugin for Base { // part of the struct somehow at init. rather than needing to parse a lot? // The internal set is bounded by: UUID_ADMIN -> UUID_ANONYMOUS // Sadly we need to allocate these to strings to make references, sigh. - let overlap: usize = cand_uuid.range(uuid_admin..uuid_anonymous).count(); + let overlap: usize = cand_uuid.range(UUID_ADMIN..UUID_ANONYMOUS).count(); if overlap != 0 { admin_error!( "uuid from protected system UUID range found in create set! {:?}", @@ -109,10 +104,10 @@ impl Plugin for Base { } } - if cand_uuid.contains(&uuid_does_not_exist) { + if cand_uuid.contains(&UUID_DOES_NOT_EXIST) { admin_error!( "uuid \"does not exist\" found in create set! {:?}", - uuid_does_not_exist + UUID_DOES_NOT_EXIST ); return Err(OperationError::Plugin(PluginError::Base( "UUID_DOES_NOT_EXIST may not exist!".to_string(), diff --git a/kanidmd/lib/src/plugins/dyngroup.rs b/kanidmd/lib/src/plugins/dyngroup.rs index 356bab68b..2897726b0 100644 --- a/kanidmd/lib/src/plugins/dyngroup.rs +++ b/kanidmd/lib/src/plugins/dyngroup.rs @@ -681,7 +681,7 @@ mod tests { filter!(f_eq("name", PartialValue::new_iname("test_dyngroup"))), ModifyList::new_list(vec![Modify::Present( AttrString::from("member"), - Value::new_refer(*UUID_ADMIN) + Value::new_refer(UUID_ADMIN) )]), None, |_| {}, diff --git a/kanidmd/lib/src/server.rs b/kanidmd/lib/src/server.rs index a58abb1e3..bbc54f16d 100644 --- a/kanidmd/lib/src/server.rs +++ b/kanidmd/lib/src/server.rs @@ -4572,7 +4572,7 @@ mod tests { // ++ Mod domain name and name to be the old type. let me_dn = unsafe { ModifyEvent::new_internal_invalid( - filter!(f_eq("uuid", PartialValue::new_uuid(*UUID_DOMAIN_INFO))), + filter!(f_eq("uuid", PartialValue::new_uuid(UUID_DOMAIN_INFO))), ModifyList::new_list(vec![ Modify::Purged(AttrString::from("name")), Modify::Purged(AttrString::from("domain_name")), diff --git a/kanidmd/lib/src/value.rs b/kanidmd/lib/src/value.rs index 2f649b1ea..a66d06528 100644 --- a/kanidmd/lib/src/value.rs +++ b/kanidmd/lib/src/value.rs @@ -22,7 +22,7 @@ use webauthn_rs::prelude::{DeviceKey as DeviceKeyV4, Passkey as PasskeyV4}; use crate::be::dbentry::DbIdentSpn; use crate::credential::Credential; -use crate::identity::IdentityId; +use crate::identity::{AccessScope, IdentityId}; use crate::repl::cid::Cid; lazy_static! { @@ -746,6 +746,7 @@ pub struct Session { pub expiry: Option, pub issued_at: OffsetDateTime, pub issued_by: IdentityId, + pub scope: AccessScope, } /// A value is a complete unit of data for an attribute. It is made up of a PartialValue, which is diff --git a/kanidmd/lib/src/valueset/session.rs b/kanidmd/lib/src/valueset/session.rs index 49a26db99..3de1188f5 100644 --- a/kanidmd/lib/src/valueset/session.rs +++ b/kanidmd/lib/src/valueset/session.rs @@ -3,8 +3,8 @@ use std::collections::BTreeMap; use time::OffsetDateTime; -use crate::be::dbvalue::{DbValueIdentityId, DbValueSession}; -use crate::identity::IdentityId; +use crate::be::dbvalue::{DbValueAccessScopeV1, DbValueIdentityId, DbValueSession}; +use crate::identity::{AccessScope, IdentityId}; use crate::prelude::*; use crate::schema::SchemaAttribute; use crate::value::Session; @@ -37,6 +37,7 @@ impl ValueSetSession { expiry, issued_at, issued_by, + scope, } => { // Convert things. let issued_at = OffsetDateTime::parse(issued_at, time::Format::Rfc3339) @@ -78,6 +79,13 @@ impl ValueSetSession { DbValueIdentityId::V1Uuid(u) => IdentityId::User(u), }; + let scope = match scope { + DbValueAccessScopeV1::IdentityOnly => AccessScope::IdentityOnly, + DbValueAccessScopeV1::ReadOnly => AccessScope::ReadOnly, + DbValueAccessScopeV1::ReadWrite => AccessScope::ReadWrite, + DbValueAccessScopeV1::Synchronise => AccessScope::Synchronise, + }; + Some(( refer, Session { @@ -85,6 +93,7 @@ impl ValueSetSession { expiry, issued_at, issued_by, + scope, }, )) } @@ -193,6 +202,12 @@ impl ValueSetT for ValueSetSession { IdentityId::Internal => DbValueIdentityId::V1Internal, IdentityId::User(u) => DbValueIdentityId::V1Uuid(u), }, + scope: match m.scope { + AccessScope::IdentityOnly => DbValueAccessScopeV1::IdentityOnly, + AccessScope::ReadOnly => DbValueAccessScopeV1::ReadOnly, + AccessScope::ReadWrite => DbValueAccessScopeV1::ReadWrite, + AccessScope::Synchronise => DbValueAccessScopeV1::Synchronise, + }, }) .collect(), )