Permissions & multi-tenancy test suite
This commit is contained in:
@@ -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(())
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user