Permissions & multi-tenancy test suite

This commit is contained in:
mdecimus
2024-09-18 18:08:57 +02:00
parent d0303aefa8
commit e9d12aea44
12 changed files with 1186 additions and 99 deletions

View File

@@ -197,26 +197,6 @@ impl ManageDirectory for Store {
.ctx(trc::Key::Total, total));
}
}
// Tenants must provide principal names including a valid domain
if let Some(domain) = name.split('@').nth(1) {
if self
.get_principal_info(domain)
.await
.caused_by(trc::location!())?
.filter(|v| v.typ == Type::Domain && v.has_tenant_access(tenant_id.into()))
.is_some()
{
valid_domains.insert(domain.to_string());
}
}
if valid_domains.is_empty() {
return Err(error(
"Invalid principal name",
"Principal name must include a valid domain".into(),
));
}
}
// Make sure new name is not taken
@@ -228,6 +208,60 @@ impl ManageDirectory for Store {
{
return Err(err_exists(PrincipalField::Name, name));
}
// Obtain tenant id, only if no default tenant is provided
if let (Some(tenant_name), None) = (principal.take_str(PrincipalField::Tenant), tenant_id) {
tenant_id = self
.get_principal_info(&tenant_name)
.await
.caused_by(trc::location!())?
.filter(|v| v.typ == Type::Tenant)
.ok_or_else(|| not_found(tenant_name.clone()))?
.id
.into();
}
// Tenants must provide principal names including a valid domain
if let Some(tenant_id) = tenant_id {
if matches!(principal.typ, Type::Tenant) {
return Err(error(
"Invalid field",
"Tenants cannot contain a tenant field".into(),
));
}
principal.set(PrincipalField::Tenant, tenant_id);
if matches!(
principal.typ,
Type::Individual
| Type::Group
| Type::List
| Type::Role
| Type::Location
| Type::Resource
| Type::Other
) {
if let Some(domain) = name.split('@').nth(1) {
if self
.get_principal_info(domain)
.await
.caused_by(trc::location!())?
.filter(|v| v.typ == Type::Domain && v.has_tenant_access(tenant_id.into()))
.is_some()
{
valid_domains.insert(domain.to_string());
}
}
if valid_domains.is_empty() {
return Err(error(
"Invalid principal name",
"Principal name must include a valid domain assigned to the tenant".into(),
));
}
}
}
principal.set(PrincipalField::Name, name);
// Map member names
@@ -307,20 +341,6 @@ impl ManageDirectory for Store {
}
}
// Obtain tenant id
if let Some(tenant_id) = tenant_id {
principal.set(PrincipalField::Tenant, tenant_id);
} else if let Some(tenant_name) = principal.take_str(PrincipalField::Tenant) {
tenant_id = self
.get_principal_info(&tenant_name)
.await
.caused_by(trc::location!())?
.filter(|v| v.typ == Type::Tenant)
.ok_or_else(|| not_found(tenant_name.clone()))?
.id
.into();
}
// Write principal
let mut batch = BatchBuilder::new();
let pinfo_name = DynamicPrincipalInfo::new(principal.typ, tenant_id);
@@ -648,7 +668,18 @@ impl ManageDirectory for Store {
// Make sure new name is not taken
let new_name = new_name.to_lowercase();
if principal.inner.name() != new_name {
if tenant_id.is_some() {
if tenant_id.is_some()
&& matches!(
principal.inner.typ,
Type::Individual
| Type::Group
| Type::List
| Type::Role
| Type::Location
| Type::Resource
| Type::Other
)
{
if let Some(domain) = new_name.split('@').nth(1) {
if self
.get_principal_info(domain)
@@ -666,7 +697,7 @@ impl ManageDirectory for Store {
if valid_domains.is_empty() {
return Err(error(
"Invalid principal name",
"Principal name must include a valid domain".into(),
"Principal name must include a valid domain assigned to the tenant".into(),
));
}
}
@@ -1340,7 +1371,8 @@ impl ManageDirectory for Store {
|| fields.iter().any(|f| {
matches!(
f,
PrincipalField::MemberOf
PrincipalField::Tenant
| PrincipalField::MemberOf
| PrincipalField::Lists
| PrincipalField::Roles
| PrincipalField::EnabledPermissions
@@ -1353,9 +1385,7 @@ impl ManageDirectory for Store {
for mut principal in results {
if !is_done || filters.is_some() {
principal = self
.get_value::<Principal>(ValueKey::from(ValueClass::Directory(
DirectoryClass::Principal(principal.id),
)))
.query(QueryBy::Id(principal.id), map_principals)
.await
.caused_by(trc::location!())?
.ok_or_else(|| not_found(principal.name().to_string()))?;
@@ -1581,6 +1611,19 @@ impl ManageDirectory for Store {
}
}
// Map tenant name
if let Some(tenant_id) = principal.take_int(PrincipalField::Tenant) {
if fields.is_empty() || fields.contains(&PrincipalField::Tenant) {
if let Some(name) = self
.get_principal_name(tenant_id as u32)
.await
.caused_by(trc::location!())?
{
principal.set(PrincipalField::Tenant, name);
}
}
}
// Obtain used quota
if matches!(principal.typ, Type::Individual | Type::Group | Type::Tenant)
&& (fields.is_empty() || fields.contains(&PrincipalField::UsedQuota))
@@ -1659,15 +1702,19 @@ fn validate_member_of(
if expected_types.is_empty() || !expected_types.contains(&member_type) {
Err(error(
format!("Invalid {} value", field.as_str()),
format!(
"Principal {member_name:?} is not a {}.",
expected_types
.iter()
.map(|t| t.as_str().to_string())
.collect::<Vec<_>>()
.join(", ")
)
.into(),
if !expected_types.is_empty() {
format!(
"Principal {member_name:?} is not a {}.",
expected_types
.iter()
.map(|t| t.as_str().to_string())
.collect::<Vec<_>>()
.join(", ")
)
.into()
} else {
format!("Principal {member_name:?} cannot be added as a member.").into()
},
))
} else {
Ok(())

View File

@@ -92,6 +92,15 @@ impl Principal {
})
}
pub fn take_int(&mut self, key: PrincipalField) -> Option<u64> {
self.take(key).and_then(|v| match v {
PrincipalValue::Integer(i) => Some(i),
PrincipalValue::IntegerList(l) => l.into_iter().next(),
PrincipalValue::String(s) => s.parse().ok(),
PrincipalValue::StringList(l) => l.into_iter().next().and_then(|s| s.parse().ok()),
})
}
pub fn take_str_array(&mut self, key: PrincipalField) -> Option<Vec<String>> {
self.take(key).map(|v| v.into_str_array())
}
@@ -697,9 +706,19 @@ impl<'de> serde::Deserialize<'de> for Principal {
let mut principal = Principal::default();
while let Some(key) = map.next_key::<&str>()? {
let key = PrincipalField::try_parse(key).ok_or_else(|| {
serde::de::Error::custom(format!("invalid principal field: {}", key))
})?;
let key = PrincipalField::try_parse(key)
.or_else(|| {
if key == "id" {
// Ignored
Some(PrincipalField::UsedQuota)
} else {
None
}
})
.ok_or_else(|| {
serde::de::Error::custom(format!("invalid principal field: {}", key))
})?;
let value = match key {
PrincipalField::Name => PrincipalValue::String(map.next_value()?),
PrincipalField::Description
@@ -711,7 +730,6 @@ impl<'de> serde::Deserialize<'de> for Principal {
continue;
}
}
PrincipalField::Type => {
principal.typ = Type::parse(map.next_value()?).ok_or_else(|| {
serde::de::Error::custom("invalid principal type")
@@ -719,7 +737,6 @@ impl<'de> serde::Deserialize<'de> for Principal {
continue;
}
PrincipalField::Quota => map.next_value::<PrincipalValue>()?,
PrincipalField::Secrets
| PrincipalField::Emails
| PrincipalField::MemberOf
@@ -728,7 +745,16 @@ impl<'de> serde::Deserialize<'de> for Principal {
| PrincipalField::Lists
| PrincipalField::EnabledPermissions
| PrincipalField::DisabledPermissions => {
PrincipalValue::StringList(map.next_value()?)
match map.next_value::<StringOrMany>()? {
StringOrMany::One(v) => PrincipalValue::StringList(vec![v]),
StringOrMany::Many(v) => {
if !v.is_empty() {
PrincipalValue::StringList(v)
} else {
continue;
}
}
}
}
PrincipalField::UsedQuota => {
// consume and ignore
@@ -787,7 +813,56 @@ impl<'de> serde::Deserialize<'de> for StringOrU64 {
}
}
#[derive(Debug)]
enum StringOrMany {
One(String),
Many(Vec<String>),
}
impl<'de> serde::Deserialize<'de> for StringOrMany {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct StringOrManyVisitor;
impl<'de> Visitor<'de> for StringOrManyVisitor {
type Value = StringOrMany;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string or a sequence of strings")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(StringOrMany::One(value.to_string()))
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: de::SeqAccess<'de>,
{
let mut vec = Vec::new();
while let Some(value) = seq.next_element::<String>()? {
vec.push(value);
}
Ok(StringOrMany::Many(vec))
}
}
deserializer.deserialize_any(StringOrManyVisitor)
}
}
impl Permission {
pub fn all() -> impl Iterator<Item = Permission> {
(0..Permission::COUNT).filter_map(Permission::from_id)
}
pub const fn is_user_permission(&self) -> bool {
matches!(
self,