20221011 sudo mode components (#1120)

This commit is contained in:
Firstyear 2022-10-13 10:54:44 +10:00 committed by GitHub
parent d179b23476
commit 2845f8c4cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 773 additions and 158 deletions

View file

@ -191,7 +191,8 @@ kanidm service-account api-token status --name admin ACCOUNT_ID
kanidm service-account api-token status --name admin demo_service 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 ```shell
kanidm service-account api-token generate --name admin ACCOUNT_ID LABEL [EXPIRY] 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 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" To destroy (revoke) an api token you will need it's token id. This can be shown with the "status"
command. command.

View file

@ -208,10 +208,12 @@ impl KanidmClient {
id: &str, id: &str,
label: &str, label: &str,
expiry: Option<OffsetDateTime>, expiry: Option<OffsetDateTime>,
read_write: bool,
) -> Result<String, ClientError> { ) -> Result<String, ClientError> {
let new_token = ApiTokenGenerate { let new_token = ApiTokenGenerate {
label: label.to_string(), label: label.to_string(),
expiry, expiry,
read_write,
}; };
self.perform_post_request( self.perform_post_request(
format!("/v1/service_account/{}/_api_token", id).as_str(), format!("/v1/service_account/{}/_api_token", id).as_str(),

View file

@ -321,6 +321,17 @@ pub enum UiHint {
PosixAccount, 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 /// The currently authenticated user, and any required metadata for them
/// to properly authorise them. This is similar in nature to oauth and the krb /// 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 /// PAC/PAD structures. This information is transparent to clients and CAN
@ -338,8 +349,8 @@ pub struct UserAuthToken {
// may depend on the client application. // may depend on the client application.
#[serde(with = "time::serde::timestamp")] #[serde(with = "time::serde::timestamp")]
pub expiry: time::OffsetDateTime, pub expiry: time::OffsetDateTime,
pub purpose: UatPurpose,
pub uuid: Uuid, pub uuid: Uuid,
pub name: String,
pub displayname: String, pub displayname: String,
pub spn: String, pub spn: String,
pub mail_primary: Option<String>, pub mail_primary: Option<String>,
@ -349,19 +360,21 @@ pub struct UserAuthToken {
impl fmt::Display for UserAuthToken { impl fmt::Display for UserAuthToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// writeln!(f, "name: {}", self.name)?;
writeln!(f, "spn: {}", self.spn)?; writeln!(f, "spn: {}", self.spn)?;
writeln!(f, "uuid: {}", self.uuid)?; writeln!(f, "uuid: {}", self.uuid)?;
writeln!(f, "display: {}", self.displayname)?; 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 { for group in &self.groups {
writeln!(f, "group: {:?}", group.spn)?; writeln!(f, "group: {:?}", group.spn)?;
} }
/* Ok(())
for claim in &self.claims {
writeln!(f, "claim: {:?}", claim)?;
}
*/
writeln!(f, "token expiry: {}", self.expiry)
} }
} }
@ -373,6 +386,21 @@ impl PartialEq for UserAuthToken {
impl Eq 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)] #[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub struct ApiToken { pub struct ApiToken {
@ -384,6 +412,9 @@ pub struct ApiToken {
pub expiry: Option<time::OffsetDateTime>, pub expiry: Option<time::OffsetDateTime>,
#[serde(with = "time::serde::timestamp")] #[serde(with = "time::serde::timestamp")]
pub issued_at: time::OffsetDateTime, pub issued_at: time::OffsetDateTime,
// Defaults to ReadOnly if not present
#[serde(default)]
pub purpose: ApiTokenPurpose,
} }
impl fmt::Display for ApiToken { impl fmt::Display for ApiToken {
@ -422,6 +453,7 @@ pub struct ApiTokenGenerate {
pub label: String, pub label: String,
#[serde(with = "time::serde::timestamp::option")] #[serde(with = "time::serde::timestamp::option")]
pub expiry: Option<time::OffsetDateTime>, pub expiry: Option<time::OffsetDateTime>,
pub read_write: bool,
} }
// UAT will need a downcast to Entry, which adds in the claims to the entry // UAT will need a downcast to Entry, which adds in the claims to the entry

View file

@ -99,6 +99,7 @@ impl ServiceAccountOpt {
copt, copt,
label, label,
expiry, expiry,
read_write,
} => { } => {
let expiry_odt = if let Some(t) = expiry { let expiry_odt = if let Some(t) = expiry {
// Convert the time to local timezone. // Convert the time to local timezone.
@ -128,6 +129,7 @@ impl ServiceAccountOpt {
aopts.account_id.as_str(), aopts.account_id.as_str(),
label, label,
expiry_odt, expiry_odt,
*read_write,
) )
.await .await
{ {

View file

@ -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". /// 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. /// After this time the api token will no longer be valid.
expiry: Option<String>, expiry: Option<String>,
#[clap(long = "rw")]
read_write: bool,
}, },
/// Destroy / revoke an api token from this service account. Access to the /// Destroy / revoke an api token from this service account. Access to the
/// token is NOT required, only the tag/uuid of the token. /// token is NOT required, only the tag/uuid of the token.

View file

@ -425,6 +425,7 @@ impl QueryServerWriteV1 {
uuid_or_name: String, uuid_or_name: String,
label: String, label: String,
expiry: Option<OffsetDateTime>, expiry: Option<OffsetDateTime>,
read_write: bool,
eventid: Uuid, eventid: Uuid,
) -> Result<String, OperationError> { ) -> Result<String, OperationError> {
let ct = duration_from_epoch_now(); let ct = duration_from_epoch_now();
@ -449,6 +450,7 @@ impl QueryServerWriteV1 {
target, target,
label, label,
expiry, expiry,
read_write,
}; };
idms_prox_write idms_prox_write

View file

@ -455,14 +455,25 @@ pub async fn service_account_api_token_get(req: tide::Request<AppState>) -> tide
pub async fn service_account_api_token_post(mut req: tide::Request<AppState>) -> tide::Result { pub async fn service_account_api_token_post(mut req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat(); let uat = req.get_current_uat();
let uuid_or_name = req.get_url_param("id")?; 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 (eventid, hvalue) = req.new_eventid();
let res = req let res = req
.state() .state()
.qe_w_ref .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; .await;
to_tide_response(res, hvalue) to_tide_response(res, hvalue)
} }
@ -891,7 +902,7 @@ pub async fn group_get_id_unix_token(req: tide::Request<AppState>) -> tide::Resu
} }
pub async fn domain_get(req: tide::Request<AppState>) -> tide::Result { pub async fn domain_get(req: tide::Request<AppState>) -> 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 json_rest_event_get(req, filter, None).await
} }

View file

@ -1214,7 +1214,7 @@ async fn test_server_api_token_lifecycle() {
assert!(tokens.is_empty()); assert!(tokens.is_empty());
let token = rsclient 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 .await
.expect("Failed to create service account api token"); .expect("Failed to create service account api token");

View file

@ -29,13 +29,10 @@ use uuid::Uuid;
use crate::entry::{Entry, EntryCommitted, EntryInit, EntryNew, EntryReduced, EntrySealed}; use crate::entry::{Entry, EntryCommitted, EntryInit, EntryNew, EntryReduced, EntrySealed};
use crate::event::{CreateEvent, DeleteEvent, ModifyEvent, SearchEvent}; use crate::event::{CreateEvent, DeleteEvent, ModifyEvent, SearchEvent};
use crate::filter::{Filter, FilterValid, FilterValidResolved}; use crate::filter::{Filter, FilterValid, FilterValidResolved};
use crate::identity::{IdentType, IdentityId}; use crate::identity::{AccessScope, IdentType, IdentityId};
use crate::modify::Modify; use crate::modify::Modify;
use crate::prelude::*; 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_MAX: usize = 2048;
const ACP_RESOLVE_FILTER_CACHE_LOCAL: usize = 16; const ACP_RESOLVE_FILTER_CACHE_LOCAL: usize = 16;
@ -514,7 +511,17 @@ pub trait AccessControlsTransaction<'a> {
} }
IdentType::User(u) => &u.entry, 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 // First get the set of acps that apply to this receiver
let related_acp: Vec<(&AccessControlSearch, _)> = let related_acp: Vec<(&AccessControlSearch, _)> =
@ -616,7 +623,7 @@ pub trait AccessControlsTransaction<'a> {
* modify and co. * 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. // Get the relevant acps for this receiver.
let related_acp: Vec<(&AccessControlSearch, _)> = let related_acp: Vec<(&AccessControlSearch, _)> =
@ -764,7 +771,17 @@ pub trait AccessControlsTransaction<'a> {
} }
IdentType::User(u) => &u.entry, 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 // Pre-check if the no-no purge class is present
let disallow = me let disallow = me
@ -925,7 +942,17 @@ pub trait AccessControlsTransaction<'a> {
} }
IdentType::User(u) => &u.entry, 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 // Some useful references we'll use for the remainder of the operation
let create_state = self.get_create(); let create_state = self.get_create();
@ -1056,7 +1083,17 @@ pub trait AccessControlsTransaction<'a> {
} }
IdentType::User(u) => &u.entry, 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 // Some useful references we'll use for the remainder of the operation
let delete_state = self.get_delete(); 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<Entry<EntryReduced, EntryCommitted>> = $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] #[test]
fn test_access_internal_search() { fn test_access_internal_search() {
// Test that an internal search bypasses ACS // Test that an internal search bypasses ACS
@ -2010,11 +2081,8 @@ mod tests {
#[test] #[test]
fn test_access_enforce_search() { fn test_access_enforce_search() {
// Test that entries from a search are reduced by acps // Test that entries from a search are reduced by acps
let e1: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(JSON_TESTPERSON1); let ev1 = unsafe { E_TESTPERSON_1.clone().into_sealed_committed() };
let ev1 = unsafe { e1.into_sealed_committed() }; let ev2 = unsafe { E_TESTPERSON_2.clone().into_sealed_committed() };
let e2: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(JSON_TESTPERSON2);
let ev2 = unsafe { e2.into_sealed_committed() };
let r_set = vec![Arc::new(ev1.clone()), Arc::new(ev2.clone())]; 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); test_acp_search!(&se_anon, vec![acp], r_set, ex_anon);
} }
macro_rules! test_acp_search_reduce { #[test]
( fn test_access_enforce_scope_search() {
$se:expr, let _ = sketching::test_init();
$controls:expr, // Test that identities are bound by their access scope.
$entries:expr, let ev1 = unsafe { E_TESTPERSON_1.clone().into_sealed_committed() };
$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 ex_admin_some = vec![Arc::new(ev1.clone())];
let res = acw let ex_admin_none = vec![];
.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 r_set = vec![Arc::new(ev1)];
let expect_set: Vec<Entry<EntryReduced, EntryCommitted>> = $expect
.into_iter()
.map(|e| unsafe { e.into_reduced() })
.collect();
debug!("expect --> {:?}", expect_set); let se_admin_io = unsafe {
debug!("result --> {:?}", reduced); SearchEvent::new_impersonate_identity(
// should be ok, and same as expect. Identity::from_impersonate_entry_identityonly(Arc::new(
assert!(reduced == expect_set); 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#"{ #[test]
"attrs": { fn test_access_enforce_scope_search_attrs() {
"name": ["testperson1"] // 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<EntrySealedCommitted> = 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] #[test]
fn test_access_enforce_search_attrs() { fn test_access_enforce_search_attrs() {
// Test that attributes are correctly limited. // Test that attributes are correctly limited.
// In this case, we test that a user can only see "name" despite the // In this case, we test that a user can only see "name" despite the
// class and uuid being present. // class and uuid being present.
let e1: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(JSON_TESTPERSON1); let ev1 = unsafe { E_TESTPERSON_1.clone().into_sealed_committed() };
let ev1 = unsafe { e1.into_sealed_committed() };
let r_set = vec![Arc::new(ev1.clone())]; let r_set = vec![Arc::new(ev1.clone())];
let ex1: Entry<EntryInit, EntryNew> = let exv1 = unsafe { E_TESTPERSON_1_REDUCED.clone().into_sealed_committed() };
Entry::unsafe_from_entry_str(JSON_TESTPERSON1_REDUCED);
let exv1 = unsafe { ex1.into_sealed_committed() };
let ex_anon = vec![exv1.clone()]; let ex_anon = vec![exv1.clone()];
let se_anon = unsafe { let se_anon = unsafe {
@ -2133,13 +2289,11 @@ mod tests {
// Test that attributes are correctly limited by the request. // Test that attributes are correctly limited by the request.
// In this case, we test that a user can only see "name" despite the // In this case, we test that a user can only see "name" despite the
// class and uuid being present. // class and uuid being present.
let e1: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(JSON_TESTPERSON1); let ev1 = unsafe { E_TESTPERSON_1.clone().into_sealed_committed() };
let ev1 = unsafe { e1.into_sealed_committed() };
let r_set = vec![Arc::new(ev1.clone())]; let r_set = vec![Arc::new(ev1.clone())];
let ex1: Entry<EntryInit, EntryNew> = let exv1 = unsafe { E_TESTPERSON_1_REDUCED.clone().into_sealed_committed() };
Entry::unsafe_from_entry_str(JSON_TESTPERSON1_REDUCED);
let exv1 = unsafe { ex1.into_sealed_committed() };
let ex_anon = vec![exv1.clone()]; let ex_anon = vec![exv1.clone()];
let mut se_anon = unsafe { let mut se_anon = unsafe {
@ -2194,8 +2348,7 @@ mod tests {
#[test] #[test]
fn test_access_enforce_modify() { fn test_access_enforce_modify() {
let e1: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(JSON_TESTPERSON1); let ev1 = unsafe { E_TESTPERSON_1.clone().into_sealed_committed() };
let ev1 = unsafe { e1.into_sealed_committed() };
let r_set = vec![Arc::new(ev1.clone())]; let r_set = vec![Arc::new(ev1.clone())];
// Name present // Name present
@ -2331,6 +2484,64 @@ mod tests {
test_acp_modify!(&me_rem_class, vec![acp_deny.clone()], &r_set, false); 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 { macro_rules! test_acp_create {
( (
$ce:expr, $ce:expr,
@ -2449,6 +2660,51 @@ mod tests {
test_acp_create!(&ce_admin, vec![acp, acp2], &r4_set, false); test_acp_create!(&ce_admin, vec![acp, acp2], &r4_set, false);
} }
#[test]
fn test_access_enforce_scope_create() {
let ev1: Entry<EntryInit, EntryNew> = 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 { macro_rules! test_acp_delete {
( (
$de:expr, $de:expr,
@ -2474,8 +2730,7 @@ mod tests {
#[test] #[test]
fn test_access_enforce_delete() { fn test_access_enforce_delete() {
let e1: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(JSON_TESTPERSON1); let ev1 = unsafe { E_TESTPERSON_1.clone().into_sealed_committed() };
let ev1 = unsafe { e1.into_sealed_committed() };
let r_set = vec![Arc::new(ev1.clone())]; let r_set = vec![Arc::new(ev1.clone())];
let de_admin = unsafe { let de_admin = unsafe {
@ -2509,6 +2764,46 @@ mod tests {
test_acp_delete!(&de_anon, vec![acp], &r_set, false); 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 { macro_rules! test_acp_effective_permissions {
( (
$ident:expr, $ident:expr,
@ -2541,7 +2836,11 @@ mod tests {
fn test_access_effective_permission_check_1() { fn test_access_effective_permission_check_1() {
let _ = sketching::test_init(); 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<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(JSON_TESTPERSON1); let e1: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(JSON_TESTPERSON1);
let ev1 = unsafe { e1.into_sealed_committed() }; let ev1 = unsafe { e1.into_sealed_committed() };
@ -2579,7 +2878,11 @@ mod tests {
fn test_access_effective_permission_check_2() { fn test_access_effective_permission_check_2() {
let _ = sketching::test_init(); 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<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(JSON_TESTPERSON1); let e1: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(JSON_TESTPERSON1);
let ev1 = unsafe { e1.into_sealed_committed() }; let ev1 = unsafe { e1.into_sealed_committed() };

View file

@ -358,6 +358,19 @@ pub struct DbValueOauthScopeMapV1 {
pub data: Vec<String>, pub data: Vec<String>,
} }
#[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)] #[derive(Serialize, Deserialize, Debug)]
pub enum DbValueIdentityId { pub enum DbValueIdentityId {
#[serde(rename = "v1i")] #[serde(rename = "v1i")]
@ -379,6 +392,8 @@ pub enum DbValueSession {
issued_at: String, issued_at: String,
#[serde(rename = "b")] #[serde(rename = "b")]
issued_by: DbValueIdentityId, issued_by: DbValueIdentityId,
#[serde(rename = "s", default)]
scope: DbValueAccessScopeV1,
}, },
} }

View file

@ -1,4 +1,11 @@
use crate::constants::uuids::*;
///! Constant Entries for the IDM ///! 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. /// Builtin System Admin account.
pub const JSON_ADMIN_V1: &str = r#"{ 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. /// Builtin IDM Admin account.
pub const JSON_IDM_ADMIN_V1: &str = r#"{ pub const JSON_IDM_ADMIN_V1: &str = r#"{
"attrs": { "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 ============ // ============ TEST DATA ============
#[cfg(test)]
pub const UUID_TESTPERSON_1: Uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
#[cfg(test)] #[cfg(test)]
pub const JSON_TESTPERSON1: &str = r#"{ pub const JSON_TESTPERSON1: &str = r#"{
"attrs": { "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)] #[cfg(test)]
pub const JSON_TESTPERSON2: &str = r#"{ pub const JSON_TESTPERSON2: &str = r#"{
"attrs": { "attrs": {
@ -527,3 +568,17 @@ pub const JSON_TESTPERSON2: &str = r#"{
"uuid": ["538faac7-4d29-473b-a59d-23023ac19955"] "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))
);
}

View file

@ -4,6 +4,7 @@ use uuid::{uuid, Uuid};
// Built in group and account ranges. // Built in group and account ranges.
pub const STR_UUID_ADMIN: &str = "00000000-0000-0000-0000-000000000000"; 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_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_READ_PRIV: Uuid = uuid!("00000000-0000-0000-0000-000000000002");
pub const _UUID_IDM_PEOPLE_WRITE_PRIV: Uuid = uuid!("00000000-0000-0000-0000-000000000003"); 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. // 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 UUID_SYSTEM_INFO: Uuid = uuid!("00000000-0000-0000-0000-ffffff000001");
pub const STR_UUID_DOMAIN_INFO: &str = "00000000-0000-0000-0000-ffffff000025"; 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. // 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 // End of system ranges
pub const UUID_DOES_NOT_EXIST: Uuid = uuid!("00000000-0000-0000-0000-fffffffffffe"); pub const UUID_DOES_NOT_EXIST: Uuid = uuid!("00000000-0000-0000-0000-fffffffffffe");
pub const UUID_ANONYMOUS: Uuid = uuid!("00000000-0000-0000-0000-ffffffffffff"); 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();
}

View file

@ -34,12 +34,14 @@ lazy_static! {
pub static ref PVCLASS_SYSTEM_INFO: PartialValue = PartialValue::new_class("system_info"); 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_SYSTEM_CONFIG: PartialValue = PartialValue::new_class("system_config");
pub static ref PVCLASS_TOMBSTONE: PartialValue = PartialValue::new_class("tombstone"); 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_DOMAIN_INFO: Value = Value::new_class("domain_info");
pub static ref CLASS_DYNGROUP: Value = Value::new_class("dyngroup"); pub static ref CLASS_DYNGROUP: Value = Value::new_class("dyngroup");
pub static ref CLASS_MEMBEROF: Value = Value::new_class("memberof"); pub static ref CLASS_MEMBEROF: Value = Value::new_class("memberof");
pub static ref CLASS_OBJECT: Value = Value::new_class("object"); pub static ref CLASS_OBJECT: Value = Value::new_class("object");
pub static ref CLASS_RECYCLED: Value = Value::new_class("recycled"); 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: Value = Value::new_class("system");
pub static ref CLASS_SYSTEM_CONFIG: Value = Value::new_class("system_config"); pub static ref CLASS_SYSTEM_CONFIG: Value = Value::new_class("system_config");
pub static ref CLASS_SYSTEM_INFO: Value = Value::new_class("system_info"); pub static ref CLASS_SYSTEM_INFO: Value = Value::new_class("system_info");

View file

@ -85,8 +85,10 @@ use crate::valueset::{self, ValueSet};
// } // }
// //
pub type EntryInitNew = Entry<EntryInit, EntryNew>;
pub type EntrySealedCommitted = Entry<EntrySealed, EntryCommitted>; pub type EntrySealedCommitted = Entry<EntrySealed, EntryCommitted>;
pub type EntryInvalidCommitted = Entry<EntryInvalid, EntryCommitted>; pub type EntryInvalidCommitted = Entry<EntryInvalid, EntryCommitted>;
pub type EntryReducedCommitted = Entry<EntryReduced, EntryCommitted>;
pub type EntryTuple = (Arc<EntrySealedCommitted>, EntryInvalidCommitted); pub type EntryTuple = (Arc<EntrySealedCommitted>, EntryInvalidCommitted);
// Entry should have a lifecycle of types. This is Raw (modifiable) and Entry (verified). // Entry should have a lifecycle of types. This is Raw (modifiable) and Entry (verified).

View file

@ -204,8 +204,9 @@ impl SearchEvent {
// Just impersonate the account with no filter changes. // Just impersonate the account with no filter changes.
#[cfg(test)] #[cfg(test)]
pub unsafe fn new_impersonate_entry_ser(e: &str, filter: Filter<FilterInvalid>) -> Self { pub unsafe fn new_impersonate_entry_ser(e: &str, filter: Filter<FilterInvalid>) -> Self {
let ei: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(e);
SearchEvent { 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: filter.clone().into_valid(),
filter_orig: filter.into_valid(), filter_orig: filter.into_valid(),
attrs: None, attrs: None,
@ -218,7 +219,17 @@ impl SearchEvent {
filter: Filter<FilterInvalid>, filter: Filter<FilterInvalid>,
) -> Self { ) -> Self {
SearchEvent { 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<FilterInvalid>) -> Self {
SearchEvent {
ident,
filter: filter.clone().into_valid(), filter: filter.clone().into_valid(),
filter_orig: filter.into_valid(), filter_orig: filter.into_valid(),
attrs: None, attrs: None,
@ -247,7 +258,7 @@ impl SearchEvent {
let filter_orig = filter.into_valid(); let filter_orig = filter.into_valid();
let filter = filter_orig.clone().into_recycled(); let filter = filter_orig.clone().into_recycled();
SearchEvent { SearchEvent {
ident: Identity::from_impersonate_entry(e), ident: Identity::from_impersonate_entry_readonly(e),
filter, filter,
filter_orig, filter_orig,
attrs: None, attrs: None,
@ -261,7 +272,7 @@ impl SearchEvent {
filter: Filter<FilterInvalid>, filter: Filter<FilterInvalid>,
) -> Self { ) -> Self {
SearchEvent { SearchEvent {
ident: Identity::from_impersonate_entry(e), ident: Identity::from_impersonate_entry_readonly(e),
filter: filter.clone().into_valid().into_ignore_hidden(), filter: filter.clone().into_valid().into_ignore_hidden(),
filter_orig: filter.into_valid(), filter_orig: filter.into_valid(),
attrs: None, attrs: None,
@ -345,18 +356,26 @@ impl CreateEvent {
} }
} }
// Is this an internal only function?
#[cfg(test)] #[cfg(test)]
pub unsafe fn new_impersonate_entry_ser( pub unsafe fn new_impersonate_entry_ser(
e: &str, e: &str,
entries: Vec<Entry<EntryInit, EntryNew>>, entries: Vec<Entry<EntryInit, EntryNew>>,
) -> Self { ) -> Self {
let ei: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(e);
CreateEvent { CreateEvent {
ident: Identity::from_impersonate_entry_ser(e), ident: Identity::from_impersonate_entry_readwrite(Arc::new(ei.into_sealed_committed())),
entries, entries,
} }
} }
#[cfg(test)]
pub fn new_impersonate_identity(
ident: Identity,
entries: Vec<Entry<EntryInit, EntryNew>>,
) -> Self {
CreateEvent { ident, entries }
}
pub fn new_internal(entries: Vec<Entry<EntryInit, EntryNew>>) -> Self { pub fn new_internal(entries: Vec<Entry<EntryInit, EntryNew>>) -> Self {
CreateEvent { CreateEvent {
ident: Identity::from_internal(), ident: Identity::from_internal(),
@ -447,16 +466,28 @@ impl DeleteEvent {
filter: Filter<FilterInvalid>, filter: Filter<FilterInvalid>,
) -> Self { ) -> Self {
DeleteEvent { DeleteEvent {
ident: Identity::from_impersonate_entry(e), ident: Identity::from_impersonate_entry_readwrite(e),
filter: filter.clone().into_valid(), filter: filter.clone().into_valid(),
filter_orig: filter.into_valid(), filter_orig: filter.into_valid(),
} }
} }
#[cfg(test)]
pub fn new_impersonate_identity(ident: Identity, filter: Filter<FilterInvalid>) -> Self {
unsafe {
DeleteEvent {
ident,
filter: filter.clone().into_valid(),
filter_orig: filter.into_valid(),
}
}
}
#[cfg(test)] #[cfg(test)]
pub unsafe fn new_impersonate_entry_ser(e: &str, filter: Filter<FilterInvalid>) -> Self { pub unsafe fn new_impersonate_entry_ser(e: &str, filter: Filter<FilterInvalid>) -> Self {
let ei: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(e);
DeleteEvent { 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: filter.clone().into_valid(),
filter_orig: filter.into_valid(), filter_orig: filter.into_valid(),
} }
@ -618,8 +649,23 @@ impl ModifyEvent {
filter: Filter<FilterInvalid>, filter: Filter<FilterInvalid>,
modlist: ModifyList<ModifyInvalid>, modlist: ModifyList<ModifyInvalid>,
) -> Self { ) -> Self {
let ei: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(e);
ModifyEvent { 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<FilterInvalid>,
modlist: ModifyList<ModifyInvalid>,
) -> Self {
ModifyEvent {
ident,
filter: filter.clone().into_valid(), filter: filter.clone().into_valid(),
filter_orig: filter.into_valid(), filter_orig: filter.into_valid(),
modlist: modlist.into_valid(), modlist: modlist.into_valid(),
@ -633,7 +679,7 @@ impl ModifyEvent {
modlist: ModifyList<ModifyInvalid>, modlist: ModifyList<ModifyInvalid>,
) -> Self { ) -> Self {
ModifyEvent { ModifyEvent {
ident: Identity::from_impersonate_entry(e), ident: Identity::from_impersonate_entry_readwrite(e),
filter: filter.clone().into_valid(), filter: filter.clone().into_valid(),
filter_orig: filter.into_valid(), filter_orig: filter.into_valid(),
modlist: modlist.into_valid(), modlist: modlist.into_valid(),
@ -769,7 +815,7 @@ impl ReviveRecycledEvent {
filter: Filter<FilterInvalid>, filter: Filter<FilterInvalid>,
) -> Self { ) -> Self {
ReviveRecycledEvent { ReviveRecycledEvent {
ident: Identity::from_impersonate_entry(e), ident: Identity::from_impersonate_entry_readwrite(e),
filter: filter.into_valid(), filter: filter.into_valid(),
} }
} }

View file

@ -7,6 +7,8 @@ use std::collections::BTreeSet;
use std::hash::Hash; use std::hash::Hash;
use std::sync::Arc; use std::sync::Arc;
use kanidm_proto::v1::ApiTokenPurpose;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::prelude::*; 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<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),
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
/// Metadata and the entry of the current Identity which is an external account/user. /// Metadata and the entry of the current Identity which is an external account/user.
pub struct IdentUser { pub struct IdentUser {
@ -81,20 +125,24 @@ impl From<&IdentType> for IdentityId {
/// An identity that initiated an `Event`. /// An identity that initiated an `Event`.
pub struct Identity { pub struct Identity {
pub origin: IdentType, pub origin: IdentType,
// pub(crate) source:
// pub(crate) impersonate: bool,
pub(crate) scope: AccessScope,
pub(crate) limits: Limits, pub(crate) limits: Limits,
} }
impl std::fmt::Display for Identity { impl std::fmt::Display for Identity {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match &self.origin { match &self.origin {
IdentType::Internal => write!(f, "Internal"), IdentType::Internal => write!(f, "Internal ({})", self.scope),
IdentType::User(u) => { IdentType::User(u) => {
let nv = u.entry.get_uuid2spn(); let nv = u.entry.get_uuid2spn();
write!( write!(
f, f,
"User( {}, {} ) ", "User( {}, {} ) ({})",
nv.to_proto_string_clone(), 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 { pub fn from_internal() -> Self {
Identity { Identity {
origin: IdentType::Internal, origin: IdentType::Internal,
scope: AccessScope::ReadWrite,
limits: Limits::unlimited(), limits: Limits::unlimited(),
} }
} }
#[cfg(test)] #[cfg(test)]
pub fn from_impersonate_entry(entry: Arc<Entry<EntrySealed, EntryCommitted>>) -> Self { pub fn from_impersonate_entry_identityonly(
entry: Arc<Entry<EntrySealed, EntryCommitted>>,
) -> Self {
Identity { Identity {
origin: IdentType::User(IdentUser { entry }), origin: IdentType::User(IdentUser { entry }),
scope: AccessScope::IdentityOnly,
limits: Limits::unlimited(), limits: Limits::unlimited(),
} }
} }
#[cfg(test)] #[cfg(test)]
pub unsafe fn from_impersonate_entry_ser(e: &str) -> Self { pub fn from_impersonate_entry_readonly(entry: Arc<Entry<EntrySealed, EntryCommitted>>) -> Self {
let ei: Entry<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(e); Identity {
Self::from_impersonate_entry(Arc::new(ei.into_sealed_committed())) origin: IdentType::User(IdentUser { entry }),
scope: AccessScope::ReadOnly,
limits: Limits::unlimited(),
}
}
#[cfg(test)]
pub fn from_impersonate_entry_readwrite(
entry: Arc<Entry<EntrySealed, EntryCommitted>>,
) -> 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 { pub fn from_impersonate(ident: &Self) -> Self {

View file

@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet};
use std::time::Duration; use std::time::Duration;
use kanidm_proto::v1::{ use kanidm_proto::v1::{
AuthType, BackupCodesView, CredentialStatus, OperationError, UiHint, UserAuthToken, AuthType, BackupCodesView, CredentialStatus, OperationError, UatPurpose, UiHint, UserAuthToken,
}; };
use time::OffsetDateTime; use time::OffsetDateTime;
use uuid::Uuid; use uuid::Uuid;
@ -195,13 +195,17 @@ impl Account {
// TODO: Apply policy to this expiry time. // TODO: Apply policy to this expiry time.
let expiry = OffsetDateTime::unix_epoch() + ct + Duration::from_secs(AUTH_SESSION_EXPIRY); 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 { Some(UserAuthToken {
session_id, session_id,
auth_type, auth_type,
expiry, expiry,
purpose,
uuid: self.uuid, uuid: self.uuid,
name: self.name.clone(),
displayname: self.displayname.clone(), displayname: self.displayname.clone(),
spn: self.spn.clone(), spn: self.spn.clone(),
mail_primary: self.mail_primary.clone(), mail_primary: self.mail_primary.clone(),

View file

@ -234,7 +234,7 @@ impl InitCredentialUpdateIntentEvent {
target: Uuid, target: Uuid,
max_ttl: Duration, max_ttl: Duration,
) -> Self { ) -> Self {
let ident = Identity::from_impersonate_entry(e); let ident = Identity::from_impersonate_entry_readwrite(e);
InitCredentialUpdateIntentEvent { InitCredentialUpdateIntentEvent {
ident, ident,
target, target,
@ -255,7 +255,7 @@ impl InitCredentialUpdateEvent {
#[cfg(test)] #[cfg(test)]
pub fn new_impersonate_entry(e: std::sync::Arc<Entry<EntrySealed, EntryCommitted>>) -> Self { pub fn new_impersonate_entry(e: std::sync::Arc<Entry<EntrySealed, EntryCommitted>>) -> Self {
let ident = Identity::from_impersonate_entry(e); let ident = Identity::from_impersonate_entry_readwrite(e);
let target = ident let target = ident
.get_uuid() .get_uuid()
.ok_or(OperationError::InvalidState) .ok_or(OperationError::InvalidState)
@ -278,6 +278,14 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
"Initiating Credential Update Session", "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. // Is target an account? This checks for us.
let account = Account::try_from_entry_rw(entry.as_ref(), &mut self.qs_write)?; let account = Account::try_from_entry_rw(entry.as_ref(), &mut self.qs_write)?;

View file

@ -971,6 +971,13 @@ impl Oauth2ResourceServersReadTransaction {
// TODO: Can the user consent to which claims are released? Today as we don't support most // 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. // 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") { let (email, email_verified) = if scope_set.contains("email") {
if let Some(mp) = code_xchg.uat.mail_primary { if let Some(mp) = code_xchg.uat.mail_primary {
(Some(mp), Some(true)) (Some(mp), Some(true))
@ -981,13 +988,6 @@ impl Oauth2ResourceServersReadTransaction {
(None, None) (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. // TODO: If max_age was requested in the request, we MUST provide auth_time.
// amr == auth method // amr == auth method
@ -2427,7 +2427,7 @@ mod tests {
== Url::parse("https://idm.example.com/oauth2/openid/test_resource_server") == Url::parse("https://idm.example.com/oauth2/openid/test_resource_server")
.unwrap() .unwrap()
); );
assert!(oidc.sub == OidcSubject::U(*UUID_ADMIN)); assert!(oidc.sub == OidcSubject::U(UUID_ADMIN));
assert!(oidc.aud == "test_resource_server"); assert!(oidc.aud == "test_resource_server");
assert!(oidc.iat == iat); assert!(oidc.iat == iat);
assert!(oidc.nbf == Some(iat)); assert!(oidc.nbf == Some(iat));
@ -2600,7 +2600,7 @@ mod tests {
.validate(&jws_validator, iat) .validate(&jws_validator, iat)
.expect("Failed to verify oidc"); .expect("Failed to verify oidc");
assert!(oidc.sub == OidcSubject::U(*UUID_ADMIN)); assert!(oidc.sub == OidcSubject::U(UUID_ADMIN));
} }
) )
} }

View file

@ -15,8 +15,8 @@ use fernet::Fernet;
use futures::task as futures_task; use futures::task as futures_task;
use hashbrown::HashSet; use hashbrown::HashSet;
use kanidm_proto::v1::{ use kanidm_proto::v1::{
ApiToken, BackupCodesView, CredentialStatus, PasswordFeedback, RadiusAuthToken, UnixGroupToken, ApiToken, BackupCodesView, CredentialStatus, PasswordFeedback, RadiusAuthToken, UatPurpose,
UnixUserToken, UserAuthToken, UnixGroupToken, UnixUserToken, UserAuthToken,
}; };
use rand::prelude::*; use rand::prelude::*;
use tokio::sync::mpsc::{ use tokio::sync::mpsc::{
@ -31,7 +31,7 @@ use super::delayed::BackupCodeRemoval;
use super::event::ReadBackupCodeEvent; use super::event::ReadBackupCodeEvent;
use crate::credential::policy::CryptoPolicy; use crate::credential::policy::CryptoPolicy;
use crate::credential::softlock::CredSoftLock; 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::account::Account;
use crate::idm::authsession::AuthSession; use crate::idm::authsession::AuthSession;
use crate::idm::credupdatesession::CredentialUpdateSessionMutex; use crate::idm::credupdatesession::CredentialUpdateSessionMutex;
@ -610,6 +610,19 @@ pub trait IdmServerTransaction<'a> {
return Err(OperationError::SessionExpired); 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 // #64: Now apply claims from the uat into the Entry
// to allow filtering. // to allow filtering.
/* /*
@ -644,6 +657,7 @@ pub trait IdmServerTransaction<'a> {
let limits = Limits::default(); let limits = Limits::default();
Ok(Identity { Ok(Identity {
origin: IdentType::User(IdentUser { entry }), origin: IdentType::User(IdentUser { entry }),
scope,
limits, limits,
}) })
} }
@ -662,9 +676,12 @@ pub trait IdmServerTransaction<'a> {
return Err(OperationError::SessionExpired); return Err(OperationError::SessionExpired);
} }
let scope = (&apit.purpose).into();
let limits = Limits::default(); let limits = Limits::default();
Ok(Identity { Ok(Identity {
origin: IdentType::User(IdentUser { entry }), origin: IdentType::User(IdentUser { entry }),
scope,
limits, limits,
}) })
} }
@ -703,6 +720,7 @@ pub trait IdmServerTransaction<'a> {
let limits = Limits::default(); let limits = Limits::default();
Ok(Identity { Ok(Identity {
origin: IdentType::User(IdentUser { entry: anon_entry }), origin: IdentType::User(IdentUser { entry: anon_entry }),
scope: AccessScope::ReadOnly,
limits, limits,
}) })
} else { } else {
@ -3588,7 +3606,7 @@ mod tests {
let idms_prox_write = idms.proxy_write(ct.clone()); let idms_prox_write = idms.proxy_write(ct.clone());
let me_reset_tokens = unsafe { let me_reset_tokens = unsafe {
ModifyEvent::new_internal_invalid( 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![ ModifyList::new_list(vec![
Modify::Purged(AttrString::from("fernet_private_key_str")), Modify::Purged(AttrString::from("fernet_private_key_str")),
Modify::Purged(AttrString::from("es256_private_key_der")), Modify::Purged(AttrString::from("es256_private_key_der")),

View file

@ -2,7 +2,7 @@ use std::collections::BTreeMap;
use std::time::Duration; use std::time::Duration;
use compact_jwt::{Jws, JwsSigner}; use compact_jwt::{Jws, JwsSigner};
use kanidm_proto::v1::ApiToken; use kanidm_proto::v1::{ApiToken, ApiTokenPurpose};
use time::OffsetDateTime; use time::OffsetDateTime;
use crate::event::SearchEvent; use crate::event::SearchEvent;
@ -150,6 +150,8 @@ pub struct GenerateApiTokenEvent {
pub label: String, pub label: String,
// When should it expire? // When should it expire?
pub expiry: Option<time::OffsetDateTime>, pub expiry: Option<time::OffsetDateTime>,
// Is it read_write capable?
pub read_write: bool,
// Limits? // Limits?
} }
@ -161,6 +163,7 @@ impl GenerateApiTokenEvent {
target, target,
label: label.to_string(), label: label.to_string(),
expiry: expiry.map(|ct| time::OffsetDateTime::unix_epoch() + ct), expiry: expiry.map(|ct| time::OffsetDateTime::unix_epoch() + ct),
read_write: false,
} }
} }
} }
@ -209,6 +212,12 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
.clone() .clone()
.map(|odt| odt.to_offset(time::UtcOffset::UTC)); .map(|odt| odt.to_offset(time::UtcOffset::UTC));
let purpose = if gte.read_write {
ApiTokenPurpose::ReadWrite
} else {
ApiTokenPurpose::ReadOnly
};
// create a new session // create a new session
let session = Value::Session( let session = Value::Session(
session_id, session_id,
@ -220,6 +229,9 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
issued_at, issued_at,
// Who actually created this? // Who actually created this?
issued_by: gte.ident.get_event_origin_id(), 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(), label: gte.label.clone(),
expiry: gte.expiry.clone(), expiry: gte.expiry.clone(),
issued_at, issued_at,
purpose,
}); });
// modify the account to put the session onto it. // modify the account to put the session onto it.
@ -318,7 +331,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
match self.qs_read.search_ext(&srch) { match self.qs_read.search_ext(&srch) {
Ok(mut entries) => { Ok(mut entries) => {
let r = entries entries
.pop() .pop()
// get the first entry // get the first entry
.and_then(|e| { .and_then(|e| {
@ -326,21 +339,29 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
// From the entry, turn it into the value // From the entry, turn it into the value
e.get_ava_as_session_map("api_token_session").map(|smap| { e.get_ava_as_session_map("api_token_session").map(|smap| {
smap.iter() smap.iter()
.map(|(u, s)| ApiToken { .map(|(u, s)| {
account_id, s.scope
token_id: *u, .try_into()
label: s.label.clone(), .map(|purpose| ApiToken {
expiry: s.expiry.clone(), account_id,
issued_at: s.issued_at.clone(), 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::<Vec<_>>() .collect::<Result<Vec<_>, _>>()
}) })
}) })
.unwrap_or_else(|| { .unwrap_or_else(|| {
// No matching entry? Return none. // No matching entry? Return none.
Vec::new() Ok(Vec::new())
}); })
Ok(r)
} }
Err(e) => Err(e), Err(e) => Err(e),
} }

View file

@ -598,16 +598,16 @@ mod tests {
let admin_t = task::block_on(ldaps.do_bind(idms, "admin", TEST_PASSWORD)) let admin_t = task::block_on(ldaps.do_bind(idms, "admin", TEST_PASSWORD))
.unwrap() .unwrap()
.unwrap(); .unwrap();
assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN)); assert!(admin_t.effective_session == LdapSession::UnixBind(UUID_ADMIN));
let admin_t = let admin_t =
task::block_on(ldaps.do_bind(idms, "admin@example.com", TEST_PASSWORD)) task::block_on(ldaps.do_bind(idms, "admin@example.com", TEST_PASSWORD))
.unwrap() .unwrap()
.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)) let admin_t = task::block_on(ldaps.do_bind(idms, STR_UUID_ADMIN, TEST_PASSWORD))
.unwrap() .unwrap()
.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( let admin_t = task::block_on(ldaps.do_bind(
idms, idms,
"name=admin,dc=example,dc=com", "name=admin,dc=example,dc=com",
@ -615,7 +615,7 @@ mod tests {
)) ))
.unwrap() .unwrap()
.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( let admin_t = task::block_on(ldaps.do_bind(
idms, idms,
"spn=admin@example.com,dc=example,dc=com", "spn=admin@example.com,dc=example,dc=com",
@ -623,7 +623,7 @@ mod tests {
)) ))
.unwrap() .unwrap()
.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( let admin_t = task::block_on(ldaps.do_bind(
idms, idms,
format!("uuid={},dc=example,dc=com", STR_UUID_ADMIN).as_str(), format!("uuid={},dc=example,dc=com", STR_UUID_ADMIN).as_str(),
@ -631,17 +631,17 @@ mod tests {
)) ))
.unwrap() .unwrap()
.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)) let admin_t = task::block_on(ldaps.do_bind(idms, "name=admin", TEST_PASSWORD))
.unwrap() .unwrap()
.unwrap(); .unwrap();
assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN)); assert!(admin_t.effective_session == LdapSession::UnixBind(UUID_ADMIN));
let admin_t = let admin_t =
task::block_on(ldaps.do_bind(idms, "spn=admin@example.com", TEST_PASSWORD)) task::block_on(ldaps.do_bind(idms, "spn=admin@example.com", TEST_PASSWORD))
.unwrap() .unwrap()
.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( let admin_t = task::block_on(ldaps.do_bind(
idms, idms,
format!("uuid={}", STR_UUID_ADMIN).as_str(), format!("uuid={}", STR_UUID_ADMIN).as_str(),
@ -649,13 +649,13 @@ mod tests {
)) ))
.unwrap() .unwrap()
.unwrap(); .unwrap();
assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN)); assert!(admin_t.effective_session == LdapSession::UnixBind(UUID_ADMIN));
let admin_t = let admin_t =
task::block_on(ldaps.do_bind(idms, "admin,dc=example,dc=com", TEST_PASSWORD)) task::block_on(ldaps.do_bind(idms, "admin,dc=example,dc=com", TEST_PASSWORD))
.unwrap() .unwrap()
.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( let admin_t = task::block_on(ldaps.do_bind(
idms, idms,
"admin@example.com,dc=example,dc=com", "admin@example.com,dc=example,dc=com",
@ -663,7 +663,7 @@ mod tests {
)) ))
.unwrap() .unwrap()
.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( let admin_t = task::block_on(ldaps.do_bind(
idms, idms,
format!("{},dc=example,dc=com", STR_UUID_ADMIN).as_str(), format!("{},dc=example,dc=com", STR_UUID_ADMIN).as_str(),
@ -671,7 +671,7 @@ mod tests {
)) ))
.unwrap() .unwrap()
.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. // Bad password, check last to prevent softlocking of the admin account.
assert!(task::block_on(ldaps.do_bind(idms, "admin", "test")) assert!(task::block_on(ldaps.do_bind(idms, "admin", "test"))

View file

@ -68,14 +68,15 @@ pub mod prelude {
pub use crate::constants::*; pub use crate::constants::*;
pub use crate::entry::{ pub use crate::entry::{
Entry, EntryCommitted, EntryInit, EntryInvalid, EntryInvalidCommitted, EntryNew, Entry, EntryCommitted, EntryInit, EntryInitNew, EntryInvalid, EntryInvalidCommitted,
EntryReduced, EntrySealed, EntrySealedCommitted, EntryTuple, EntryValid, EntryNew, EntryReduced, EntryReducedCommitted, EntrySealed, EntrySealedCommitted,
EntryTuple, EntryValid,
}; };
pub use crate::filter::{ 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, 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, 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::modify::{m_pres, m_purge, m_remove, Modify, ModifyInvalid, ModifyList};
pub use crate::server::{ pub use crate::server::{
QueryServer, QueryServerReadTransaction, QueryServerTransaction, QueryServer, QueryServerReadTransaction, QueryServerTransaction,

View file

@ -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 // Check that the system-protected range is not in the cand_uuid, unless we are
// an internal operation. // an internal operation.
if !ce.ident.is_internal() { 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? // part of the struct somehow at init. rather than needing to parse a lot?
// The internal set is bounded by: UUID_ADMIN -> UUID_ANONYMOUS // The internal set is bounded by: UUID_ADMIN -> UUID_ANONYMOUS
// Sadly we need to allocate these to strings to make references, sigh. // 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 { if overlap != 0 {
admin_error!( admin_error!(
"uuid from protected system UUID range found in create set! {:?}", "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!( admin_error!(
"uuid \"does not exist\" found in create set! {:?}", "uuid \"does not exist\" found in create set! {:?}",
uuid_does_not_exist UUID_DOES_NOT_EXIST
); );
return Err(OperationError::Plugin(PluginError::Base( return Err(OperationError::Plugin(PluginError::Base(
"UUID_DOES_NOT_EXIST may not exist!".to_string(), "UUID_DOES_NOT_EXIST may not exist!".to_string(),

View file

@ -681,7 +681,7 @@ mod tests {
filter!(f_eq("name", PartialValue::new_iname("test_dyngroup"))), filter!(f_eq("name", PartialValue::new_iname("test_dyngroup"))),
ModifyList::new_list(vec![Modify::Present( ModifyList::new_list(vec![Modify::Present(
AttrString::from("member"), AttrString::from("member"),
Value::new_refer(*UUID_ADMIN) Value::new_refer(UUID_ADMIN)
)]), )]),
None, None,
|_| {}, |_| {},

View file

@ -4572,7 +4572,7 @@ mod tests {
// ++ Mod domain name and name to be the old type. // ++ Mod domain name and name to be the old type.
let me_dn = unsafe { let me_dn = unsafe {
ModifyEvent::new_internal_invalid( 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![ ModifyList::new_list(vec![
Modify::Purged(AttrString::from("name")), Modify::Purged(AttrString::from("name")),
Modify::Purged(AttrString::from("domain_name")), Modify::Purged(AttrString::from("domain_name")),

View file

@ -22,7 +22,7 @@ use webauthn_rs::prelude::{DeviceKey as DeviceKeyV4, Passkey as PasskeyV4};
use crate::be::dbentry::DbIdentSpn; use crate::be::dbentry::DbIdentSpn;
use crate::credential::Credential; use crate::credential::Credential;
use crate::identity::IdentityId; use crate::identity::{AccessScope, IdentityId};
use crate::repl::cid::Cid; use crate::repl::cid::Cid;
lazy_static! { lazy_static! {
@ -746,6 +746,7 @@ pub struct Session {
pub expiry: Option<OffsetDateTime>, pub expiry: Option<OffsetDateTime>,
pub issued_at: OffsetDateTime, pub issued_at: OffsetDateTime,
pub issued_by: IdentityId, 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 /// A value is a complete unit of data for an attribute. It is made up of a PartialValue, which is

View file

@ -3,8 +3,8 @@ use std::collections::BTreeMap;
use time::OffsetDateTime; use time::OffsetDateTime;
use crate::be::dbvalue::{DbValueIdentityId, DbValueSession}; use crate::be::dbvalue::{DbValueAccessScopeV1, DbValueIdentityId, DbValueSession};
use crate::identity::IdentityId; use crate::identity::{AccessScope, IdentityId};
use crate::prelude::*; use crate::prelude::*;
use crate::schema::SchemaAttribute; use crate::schema::SchemaAttribute;
use crate::value::Session; use crate::value::Session;
@ -37,6 +37,7 @@ impl ValueSetSession {
expiry, expiry,
issued_at, issued_at,
issued_by, issued_by,
scope,
} => { } => {
// Convert things. // Convert things.
let issued_at = OffsetDateTime::parse(issued_at, time::Format::Rfc3339) let issued_at = OffsetDateTime::parse(issued_at, time::Format::Rfc3339)
@ -78,6 +79,13 @@ impl ValueSetSession {
DbValueIdentityId::V1Uuid(u) => IdentityId::User(u), 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(( Some((
refer, refer,
Session { Session {
@ -85,6 +93,7 @@ impl ValueSetSession {
expiry, expiry,
issued_at, issued_at,
issued_by, issued_by,
scope,
}, },
)) ))
} }
@ -193,6 +202,12 @@ impl ValueSetT for ValueSetSession {
IdentityId::Internal => DbValueIdentityId::V1Internal, IdentityId::Internal => DbValueIdentityId::V1Internal,
IdentityId::User(u) => DbValueIdentityId::V1Uuid(u), 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(), .collect(),
) )