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

View file

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

View file

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

View file

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

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".
/// 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.

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

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_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");

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

View file

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

View file

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

View file

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

View file

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

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
// 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));
}
)
}

View file

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

View file

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

View file

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

View file

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

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

View file

@ -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,
|_| {},

View file

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

View file

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

View file

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