mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
20221011 sudo mode components (#1120)
This commit is contained in:
parent
d179b23476
commit
2845f8c4cc
|
@ -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.
|
||||
|
||||
|
|
|
@ -208,10 +208,12 @@ impl KanidmClient {
|
|||
id: &str,
|
||||
label: &str,
|
||||
expiry: Option<OffsetDateTime>,
|
||||
read_write: bool,
|
||||
) -> Result<String, ClientError> {
|
||||
let new_token = ApiTokenGenerate {
|
||||
label: label.to_string(),
|
||||
expiry,
|
||||
read_write,
|
||||
};
|
||||
self.perform_post_request(
|
||||
format!("/v1/service_account/{}/_api_token", id).as_str(),
|
||||
|
|
|
@ -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<String>,
|
||||
|
@ -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<time::OffsetDateTime>,
|
||||
#[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<time::OffsetDateTime>,
|
||||
pub read_write: bool,
|
||||
}
|
||||
|
||||
// UAT will need a downcast to Entry, which adds in the claims to the entry
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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<String>,
|
||||
#[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.
|
||||
|
|
|
@ -425,6 +425,7 @@ impl QueryServerWriteV1 {
|
|||
uuid_or_name: String,
|
||||
label: String,
|
||||
expiry: Option<OffsetDateTime>,
|
||||
read_write: bool,
|
||||
eventid: Uuid,
|
||||
) -> Result<String, OperationError> {
|
||||
let ct = duration_from_epoch_now();
|
||||
|
@ -449,6 +450,7 @@ impl QueryServerWriteV1 {
|
|||
target,
|
||||
label,
|
||||
expiry,
|
||||
read_write,
|
||||
};
|
||||
|
||||
idms_prox_write
|
||||
|
|
|
@ -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 {
|
||||
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<AppState>) -> tide::Resu
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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<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]
|
||||
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<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(JSON_TESTPERSON1);
|
||||
let ev1 = unsafe { e1.into_sealed_committed() };
|
||||
|
||||
let e2: Entry<EntryInit, EntryNew> = 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<Entry<EntryReduced, EntryCommitted>> = $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<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]
|
||||
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<EntryInit, EntryNew> = 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<EntryInit, EntryNew> =
|
||||
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<EntryInit, EntryNew> = 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<EntryInit, EntryNew> =
|
||||
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<EntryInit, EntryNew> = 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<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 {
|
||||
(
|
||||
$de:expr,
|
||||
|
@ -2474,8 +2730,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_access_enforce_delete() {
|
||||
let e1: Entry<EntryInit, EntryNew> = 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<EntryInit, EntryNew> = 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<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(JSON_TESTPERSON1);
|
||||
let ev1 = unsafe { e1.into_sealed_committed() };
|
||||
|
|
|
@ -358,6 +358,19 @@ pub struct DbValueOauthScopeMapV1 {
|
|||
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)]
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -85,8 +85,10 @@ use crate::valueset::{self, ValueSet};
|
|||
// }
|
||||
//
|
||||
|
||||
pub type EntryInitNew = Entry<EntryInit, EntryNew>;
|
||||
pub type EntrySealedCommitted = Entry<EntrySealed, EntryCommitted>;
|
||||
pub type EntryInvalidCommitted = Entry<EntryInvalid, EntryCommitted>;
|
||||
pub type EntryReducedCommitted = Entry<EntryReduced, EntryCommitted>;
|
||||
pub type EntryTuple = (Arc<EntrySealedCommitted>, EntryInvalidCommitted);
|
||||
|
||||
// Entry should have a lifecycle of types. This is Raw (modifiable) and Entry (verified).
|
||||
|
|
|
@ -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<FilterInvalid>) -> Self {
|
||||
let ei: Entry<EntryInit, EntryNew> = 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<FilterInvalid>,
|
||||
) -> 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<FilterInvalid>) -> 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<FilterInvalid>,
|
||||
) -> 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<Entry<EntryInit, EntryNew>>,
|
||||
) -> Self {
|
||||
let ei: Entry<EntryInit, EntryNew> = 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<Entry<EntryInit, EntryNew>>,
|
||||
) -> Self {
|
||||
CreateEvent { ident, entries }
|
||||
}
|
||||
|
||||
pub fn new_internal(entries: Vec<Entry<EntryInit, EntryNew>>) -> Self {
|
||||
CreateEvent {
|
||||
ident: Identity::from_internal(),
|
||||
|
@ -447,16 +466,28 @@ impl DeleteEvent {
|
|||
filter: Filter<FilterInvalid>,
|
||||
) -> 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<FilterInvalid>) -> 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<FilterInvalid>) -> Self {
|
||||
let ei: Entry<EntryInit, EntryNew> = 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<FilterInvalid>,
|
||||
modlist: ModifyList<ModifyInvalid>,
|
||||
) -> Self {
|
||||
let ei: Entry<EntryInit, EntryNew> = 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<FilterInvalid>,
|
||||
modlist: ModifyList<ModifyInvalid>,
|
||||
) -> 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<ModifyInvalid>,
|
||||
) -> 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<FilterInvalid>,
|
||||
) -> Self {
|
||||
ReviveRecycledEvent {
|
||||
ident: Identity::from_impersonate_entry(e),
|
||||
ident: Identity::from_impersonate_entry_readwrite(e),
|
||||
filter: filter.into_valid(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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)]
|
||||
/// 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<Entry<EntrySealed, EntryCommitted>>) -> Self {
|
||||
pub fn from_impersonate_entry_identityonly(
|
||||
entry: Arc<Entry<EntrySealed, EntryCommitted>>,
|
||||
) -> 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<EntryInit, EntryNew> = Entry::unsafe_from_entry_str(e);
|
||||
Self::from_impersonate_entry(Arc::new(ei.into_sealed_committed()))
|
||||
pub fn from_impersonate_entry_readonly(entry: Arc<Entry<EntrySealed, EntryCommitted>>) -> Self {
|
||||
Identity {
|
||||
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 {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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<Entry<EntrySealed, EntryCommitted>>) -> 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)?;
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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")),
|
||||
|
|
|
@ -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<time::OffsetDateTime>,
|
||||
// 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::<Vec<_>>()
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
// No matching entry? Return none.
|
||||
Vec::new()
|
||||
});
|
||||
Ok(r)
|
||||
Ok(Vec::new())
|
||||
})
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|_| {},
|
||||
|
|
|
@ -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")),
|
||||
|
|
|
@ -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<OffsetDateTime>,
|
||||
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
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue