Add the compliance permission and the Compliance Officer roles #88

Merged
jcoffey-dev merged 2 commits from feature/compliance-roles into main 2026-09-28 16:34:17 +00:00
13 changed files with 548 additions and 5 deletions
Showing only changes of commit 63adb4e2b8 - Show all commits
+6
View File
@@ -290,6 +290,12 @@ impl Default for DefaultPermissions {
default.superuser.push(permission); default.superuser.push(permission);
default.tenant.push(permission); default.tenant.push(permission);
} }
// inbuxa: personal-data catalog: the data inventory, the
// server's or, inside a tenant, the tenant's slice
Permission::SysComplianceGet => {
default.superuser.push(permission);
default.tenant.push(permission);
}
// inbuxa: AL-12: tenant administrators lock and delegate // inbuxa: AL-12: tenant administrators lock and delegate
// within their tenant // within their tenant
Permission::SysAccountLockGet Permission::SysAccountLockGet
@@ -0,0 +1,267 @@
/*
* SPDX-FileCopyrightText: 2026 Coffey Labs
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
//! The compliance roles (personal-data catalog spec, §7; settled
//! 2026-09-28): a server-level Compliance Officer, and one Compliance
//! Officer role in each tenant. A tenant's accounts can hold only roles of
//! their own tenant (MT-3), so the tenant role is made per tenant: once for
//! each tenant a server already has, and whenever a tenant is created.
//!
//! Each creation is recorded under `P` `c` in the fork's subspace, so a
//! role an administrator deletes stays deleted. A tenant's role, while
//! nobody holds it, is removed with the tenant so it doesn't block the
//! delete.
//!
//! Both read what compliance work needs and change no server setting. The
//! server-level officer also places, widens, releases and exports legal
//! holds: that is the job, and each is audited with its reason. A tenant's
//! role has no holds, which are server-level only (LH-13), and the tenant
//! ceiling keeps it within the tenant. Each role carries a user's own
//! permissions too (signing in, mail), since roles given to a person replace
//! the default user role, and a tenant's accounts can't hold the
//! server-level User role.
use registry::schema::{
enums::Permission,
prelude::ObjectType,
structs::{Role, Tenant},
};
use registry::types::map::Map;
use store::{
RegistryStore, SUBSPACE_INBUXA, Store, ValueKey,
registry::write::{RegistryWrite, RegistryWriteResult},
write::{AnyClass, BatchBuilder, ValueClass},
};
use trc::AddContext;
use types::id::Id;
/// The role's name, in the server's roles and in each tenant's.
pub const NAME: &str = "Compliance Officer";
/// Reading who and what records refer to, for both roles.
const READS: &[Permission] = &[
Permission::SysAccountGet,
Permission::SysAccountQuery,
Permission::SysMailingListGet,
Permission::SysMailingListQuery,
Permission::SysDomainGet,
Permission::SysDomainQuery,
Permission::SysTenantGet,
Permission::SysTenantQuery,
Permission::SysRoleGet,
Permission::SysRoleQuery,
];
/// What the server-level officer holds besides [`READS`].
const OFFICER: &[Permission] = &[
Permission::SysComplianceGet,
Permission::SysAuditGet,
Permission::SysAuditExport,
Permission::SysLegalHoldGet,
Permission::SysLegalHoldCreate,
Permission::SysLegalHoldUpdate,
Permission::SysLegalHoldExport,
Permission::SysAccountLockGet,
];
/// What a tenant's officer holds besides [`READS`].
const TENANT_OFFICER: &[Permission] = &[
Permission::SysComplianceGet,
Permission::SysAuditGet,
Permission::SysAuditExport,
Permission::SysAccountLockGet,
];
fn role(own: &[Permission], tenant: Option<Id>) -> Role {
let mut permissions = crate::auth::permissions::DefaultPermissions::default().user;
for permission in own.iter().chain(READS) {
if !permissions.contains(permission) {
permissions.push(*permission);
}
}
Role {
description: NAME.into(),
enabled_permissions: Map::new(permissions),
member_tenant_id: tenant,
..Default::default()
}
}
/// The server-level Compliance Officer role.
pub fn officer_role() -> Role {
role(OFFICER, None)
}
/// A tenant's Compliance Officer role.
pub fn tenant_role(tenant: Id) -> Role {
role(TENANT_OFFICER, Some(tenant))
}
/// Where a creation is recorded: the server's role, or a tenant's. The value
/// is the role's id.
fn created_key(tenant: Option<Id>) -> ValueClass {
let mut key = b"Pc".to_vec();
if let Some(tenant) = tenant {
key.extend_from_slice(&tenant.id().to_be_bytes());
}
ValueClass::Any(AnyClass {
subspace: SUBSPACE_INBUXA,
key,
})
}
async fn recorded(data: &Store, tenant: Option<Id>) -> trc::Result<Option<Id>> {
Ok(data
.get_value::<u64>(ValueKey::from(created_key(tenant)))
.await
.caused_by(trc::location!())?
.map(Id::from))
}
async fn record(data: &Store, tenant: Option<Id>, role: Option<Id>) -> trc::Result<()> {
let mut batch = BatchBuilder::new();
match role {
Some(role) => batch.set(created_key(tenant), role.id().to_be_bytes().to_vec()),
None => batch.clear(created_key(tenant)),
};
data.write(batch.build_all())
.await
.caused_by(trc::location!())
.map(|_| ())
}
/// Creates a role, unless one was created for this place before, and records
/// it. Returns the new role's id.
async fn create_once(
registry: &RegistryStore,
data: &Store,
tenant: Option<Id>,
role: Role,
) -> trc::Result<Option<Id>> {
if recorded(data, tenant).await?.is_some() {
return Ok(None);
}
match registry.write(RegistryWrite::insert(&role.into())).await? {
RegistryWriteResult::Success(id) => {
record(data, tenant, Some(id)).await?;
Ok(Some(id))
}
err => {
trc::error!(
trc::EventType::Registry(trc::RegistryEvent::ValidationError)
.into_err()
.details(format!("Failed to create the {NAME} role: {err}"))
);
Ok(None)
}
}
}
/// Once per server: the officer role, and one in each tenant it already has.
pub async fn ensure_compliance_roles(registry: &RegistryStore, data: &Store) -> trc::Result<()> {
create_once(registry, data, None, officer_role()).await?;
for tenant in registry.list::<Tenant>().await? {
let tenant = Id::from(tenant.id.id());
create_once(registry, data, Some(tenant), tenant_role(tenant)).await?;
}
Ok(())
}
/// A new tenant gets its Compliance Officer role.
pub async fn tenant_created(registry: &RegistryStore, data: &Store, tenant: Id) -> trc::Result<()> {
create_once(registry, data, Some(tenant), tenant_role(tenant)).await.map(|_| ())
}
/// Before a tenant is deleted: removes its Compliance Officer role if nobody
/// holds it, so the role doesn't block the delete. Returns whether it did,
/// so a delete refused for another reason can put it back.
pub async fn tenant_deleting(registry: &RegistryStore, data: &Store, tenant: Id) -> trc::Result<bool> {
let Some(role) = recorded(data, Some(tenant)).await? else {
return Ok(false);
};
match registry
.write(RegistryWrite::delete(ObjectType::Role.id(role)))
.await?
{
RegistryWriteResult::Success(_) | RegistryWriteResult::NotFound { .. } => {
record(data, Some(tenant), None).await?;
Ok(true)
}
// Held by someone: the tenant's delete is refused for that anyway
_ => Ok(false),
}
}
/// A tenant's delete was refused after its role went: the role comes back.
pub async fn tenant_kept(registry: &RegistryStore, data: &Store, tenant: Id) -> trc::Result<()> {
tenant_created(registry, data, tenant).await
}
#[cfg(test)]
mod tests {
use super::*;
use registry::types::EnumImpl;
fn permissions(role: &Role) -> Vec<Permission> {
role.enabled_permissions.iter().copied().collect()
}
#[test]
fn neither_role_changes_a_setting() {
let user = crate::auth::permissions::DefaultPermissions::default().user;
for role in [officer_role(), tenant_role(Id::from(7u64))] {
let all = permissions(&role);
for permission in user.iter() {
assert!(all.contains(permission), "a user's own {permission:?}");
}
// Beyond what any user holds for their own account
for permission in all.into_iter().filter(|p| !user.contains(p)) {
let name = permission.as_str();
let holds = name.starts_with("sysLegalHold");
assert!(
!(name.ends_with("Update") && !holds)
&& !(name.ends_with("Create") && !holds)
&& !name.ends_with("Destroy")
&& permission != Permission::Impersonate
&& permission != Permission::FetchAnyBlob,
"{} holds {name}",
role.description
);
}
}
}
#[test]
fn the_officer_places_and_releases_holds_a_tenants_does_not() {
let officer = permissions(&officer_role());
let tenant = tenant_role(Id::from(7u64));
assert_eq!(tenant.member_tenant_id, Some(Id::from(7u64)));
let tenant = permissions(&tenant);
for hold in [
Permission::SysLegalHoldGet,
Permission::SysLegalHoldCreate,
Permission::SysLegalHoldUpdate,
Permission::SysLegalHoldExport,
] {
assert!(officer.contains(&hold));
assert!(!tenant.contains(&hold));
}
for both in [Permission::SysComplianceGet, Permission::SysAuditGet, Permission::SysAccountGet] {
assert!(officer.contains(&both) && tenant.contains(&both));
}
assert!(!officer.contains(&Permission::SysAuditSettingsUpdate));
}
#[test]
fn records_are_per_place() {
let ValueClass::Any(server) = created_key(None) else { panic!() };
let ValueClass::Any(a) = created_key(Some(Id::from(1u64))) else { panic!() };
let ValueClass::Any(b) = created_key(Some(Id::from(2u64))) else { panic!() };
assert_eq!(server.key, b"Pc");
assert_ne!(a.key, b.key);
assert!(a.key.starts_with(b"Pc"));
}
}
+2
View File
@@ -482,6 +482,8 @@ async fn insert_safe_defaults(bp: &mut Bootstrap) -> trc::Result<()> {
// inbuxa: administrator roles stored before a permission existed get it once // inbuxa: administrator roles stored before a permission existed get it once
super::granted_permissions::grant_new_admin_permissions(bp).await?; super::granted_permissions::grant_new_admin_permissions(bp).await?;
// inbuxa: personal-data catalog: the compliance roles, once per server
super::compliance_roles::ensure_compliance_roles(&bp.registry, &bp.data_store).await?;
if bp if bp
.registry .registry
@@ -30,7 +30,8 @@ use types::id::Id;
/// Granted to the default administrator roles: "Explain this" /// Granted to the default administrator roles: "Explain this"
/// (ai-explain spec, EX-4: superuser by default), the audit log, account /// (ai-explain spec, EX-4: superuser by default), the audit log, account
/// locks and legal holds (audit-hold-lock spec, AU-9, AL-12, LH-13). /// locks and legal holds (audit-hold-lock spec, AU-9, AL-12, LH-13), and
/// the data inventory (personal-data catalog spec).
const ADMIN_GRANTS: &[Permission] = &[ const ADMIN_GRANTS: &[Permission] = &[
Permission::SysAiExplain, Permission::SysAiExplain,
Permission::SysAuditGet, Permission::SysAuditGet,
@@ -44,11 +45,12 @@ const ADMIN_GRANTS: &[Permission] = &[
Permission::SysLegalHoldCreate, Permission::SysLegalHoldCreate,
Permission::SysLegalHoldUpdate, Permission::SysLegalHoldUpdate,
Permission::SysLegalHoldExport, Permission::SysLegalHoldExport,
Permission::SysComplianceGet,
]; ];
/// Granted to the default tenant administrator roles: reading and exporting /// Granted to the default tenant administrator roles: reading and exporting
/// the tenant's audit log (AU-9), and locking and delegating its accounts /// the tenant's audit log (AU-9), locking and delegating its accounts
/// (AL-12). /// (AL-12), and the tenant's slice of the data inventory.
const TENANT_GRANTS: &[Permission] = &[ const TENANT_GRANTS: &[Permission] = &[
Permission::SysAuditGet, Permission::SysAuditGet,
Permission::SysAuditExport, Permission::SysAuditExport,
@@ -56,6 +58,7 @@ const TENANT_GRANTS: &[Permission] = &[
Permission::SysAccountLockCreate, Permission::SysAccountLockCreate,
Permission::SysAccountLockUpdate, Permission::SysAccountLockUpdate,
Permission::SysAccountLockDestroy, Permission::SysAccountLockDestroy,
Permission::SysComplianceGet,
]; ];
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq)]
+1
View File
@@ -18,6 +18,7 @@ use utils::HttpLimitResponse;
pub mod application; pub mod application;
pub mod backup; pub mod backup;
pub mod boot; pub mod boot;
pub mod compliance_roles; // inbuxa: personal-data catalog, the compliance roles
pub mod console; pub mod console;
pub mod defaults; pub mod defaults;
pub mod first_party; pub mod first_party;
+28
View File
@@ -747,6 +747,16 @@ impl RegistrySet for Server {
if let ObjectInner::MaskedEmail(mask) = &new_object.inner { if let ObjectInner::MaskedEmail(mask) = &new_object.inner {
crate::inbuxa::masked_email::created(self, id, mask).await?; crate::inbuxa::masked_email::created(self, id, mask).await?;
} }
// inbuxa: personal-data catalog: a new tenant gets its
// Compliance Officer role
if matches!(new_object.inner, ObjectInner::Tenant(_)) {
common::manager::compliance_roles::tenant_created(
self.registry(),
&self.core.storage.data,
id,
)
.await?;
}
response.object.insert(Property::Id, RegistryValue::Id(id)); response.object.insert(Property::Id, RegistryValue::Id(id));
set.response set.response
.created .created
@@ -800,6 +810,15 @@ impl RegistrySet for Server {
&& object.inner.account_id() != Some(Id::from(set.account_id)))) && object.inner.account_id() != Some(Id::from(set.account_id))))
}) })
{ {
// inbuxa: personal-data catalog: a tenant's compliance
// role, while nobody holds it, goes first
let role_released = matches!(object.inner, ObjectInner::Tenant(_))
&& common::manager::compliance_roles::tenant_deleting(
self.registry(),
&self.core.storage.data,
id,
)
.await?;
match self match self
.registry() .registry()
.write(RegistryWrite::Delete { .write(RegistryWrite::Delete {
@@ -863,6 +882,15 @@ impl RegistrySet for Server {
set.response.destroyed.push(id); set.response.destroyed.push(id);
} }
err => { err => {
// inbuxa: refused for another reason: the role comes back
if role_released {
common::manager::compliance_roles::tenant_kept(
self.registry(),
&self.core.storage.data,
id,
)
.await?;
}
set.response.not_destroyed.append(id, map_write_error(err)); set.response.not_destroyed.append(id, map_write_error(err));
} }
} }
+2
View File
@@ -1744,6 +1744,8 @@ pub enum Permission {
SysLegalHoldCreate = 670, SysLegalHoldCreate = 670,
SysLegalHoldUpdate = 671, SysLegalHoldUpdate = 671,
SysLegalHoldExport = 672, SysLegalHoldExport = 672,
// inbuxa: personal-data catalog, the data inventory and compliance overview
SysComplianceGet = 673,
SysAccountGet = 219, SysAccountGet = 219,
SysAccountCreate = 220, SysAccountCreate = 220,
SysAccountUpdate = 221, SysAccountUpdate = 221,
+4 -1
View File
@@ -7084,6 +7084,7 @@ impl EnumImpl for Permission {
b"sysLegalHoldCreate" => Permission::SysLegalHoldCreate, b"sysLegalHoldCreate" => Permission::SysLegalHoldCreate,
b"sysLegalHoldUpdate" => Permission::SysLegalHoldUpdate, b"sysLegalHoldUpdate" => Permission::SysLegalHoldUpdate,
b"sysLegalHoldExport" => Permission::SysLegalHoldExport, b"sysLegalHoldExport" => Permission::SysLegalHoldExport,
b"sysComplianceGet" => Permission::SysComplianceGet,
b"sysAccountGet" => Permission::SysAccountGet, b"sysAccountGet" => Permission::SysAccountGet,
b"sysAccountCreate" => Permission::SysAccountCreate, b"sysAccountCreate" => Permission::SysAccountCreate,
b"sysAccountUpdate" => Permission::SysAccountUpdate, b"sysAccountUpdate" => Permission::SysAccountUpdate,
@@ -7773,6 +7774,7 @@ impl EnumImpl for Permission {
Permission::SysLegalHoldCreate => "sysLegalHoldCreate", Permission::SysLegalHoldCreate => "sysLegalHoldCreate",
Permission::SysLegalHoldUpdate => "sysLegalHoldUpdate", Permission::SysLegalHoldUpdate => "sysLegalHoldUpdate",
Permission::SysLegalHoldExport => "sysLegalHoldExport", Permission::SysLegalHoldExport => "sysLegalHoldExport",
Permission::SysComplianceGet => "sysComplianceGet",
Permission::SysAccountGet => "sysAccountGet", Permission::SysAccountGet => "sysAccountGet",
Permission::SysAccountCreate => "sysAccountCreate", Permission::SysAccountCreate => "sysAccountCreate",
Permission::SysAccountUpdate => "sysAccountUpdate", Permission::SysAccountUpdate => "sysAccountUpdate",
@@ -8455,6 +8457,7 @@ impl EnumImpl for Permission {
670 => Some(Permission::SysLegalHoldCreate), 670 => Some(Permission::SysLegalHoldCreate),
671 => Some(Permission::SysLegalHoldUpdate), 671 => Some(Permission::SysLegalHoldUpdate),
672 => Some(Permission::SysLegalHoldExport), 672 => Some(Permission::SysLegalHoldExport),
673 => Some(Permission::SysComplianceGet),
219 => Some(Permission::SysAccountGet), 219 => Some(Permission::SysAccountGet),
220 => Some(Permission::SysAccountCreate), 220 => Some(Permission::SysAccountCreate),
221 => Some(Permission::SysAccountUpdate), 221 => Some(Permission::SysAccountUpdate),
@@ -8899,7 +8902,7 @@ impl EnumImpl for Permission {
} }
} }
const COUNT: usize = 673; const COUNT: usize = 674;
} }
impl serde::Serialize for Permission { impl serde::Serialize for Permission {
@@ -526,6 +526,17 @@ it (Settled 3). It has no hold permissions, since legal holds are
server-only by the tenant ceiling (LH-13), and it sees the tenant's slice of server-only by the tenant ceiling (LH-13), and it sees the tenant's slice of
the inventory only (§6). the inventory only (§6).
**As built (2026-09-28).** A tenant's accounts can hold only roles of
their own tenant (MT-3), so the tenant role can't be one server-level role:
each tenant gets its own "Compliance Officer" role, made once for every
tenant a server has and whenever a tenant is created; while nobody holds
it, it is removed with its tenant so it doesn't block the delete. For the
same reason (a tenant's accounts can't hold the server-level User role),
both roles carry a user's own permissions as well, and are given in place
of the default user role. Creations are recorded under `P` `c`, so a role
an administrator deletes stays deleted
(`crates/common/src/manager/compliance_roles.rs`).
### Reaching existing servers ### Reaching existing servers
New permissions get ids 673 onward and `COUNT` grows (`enums.rs`, New permissions get ids 673 onward and `COUNT` grows (`enums.rs`,
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MiWRzsz0AHdRshG_8JimktRqBO_pHGAUBHFcfV5jwI4 nh0Vx79fhlQAOjCC9it831hBytCjhGPloPm9qidUhGc
+219
View File
@@ -0,0 +1,219 @@
/*
* SPDX-FileCopyrightText: 2026 Coffey Labs
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
//! The compliance roles (personal-data catalog spec, §7): each made once,
//! the officer reads the audit log and places holds but changes no setting,
//! and the tenant officer reaches no holds.
use crate::utils::{
account::Account,
server::{TestServer, TestServerBuilder},
};
use registry::schema::{
prelude::{ObjectType, Property},
structs::{
CertificateManagement, CustomRoles, DkimManagement, DnsManagement, Domain, Role, Tenant,
UserRoles,
},
};
use registry::types::map::Map;
use serde_json::{Value, json};
use types::id::Id;
const USING: &[&str] = &[
"urn:ietf:params:jmap:core",
"urn:inbuxa:jmap",
"urn:inbuxa:jmap:registry",
];
async fn call(account: &Account, method: &str, mut arguments: Value) -> (String, Value) {
if arguments.get("accountId").is_none() {
arguments["accountId"] = account.id_string().into();
}
let response = account.jmap_request(USING, json!([[method, arguments, "0"]])).await;
let call = response
.0
.pointer("/methodResponses/0")
.cloned()
.unwrap_or_else(|| panic!("{method}: {}", response.0));
(call[0].as_str().unwrap_or_default().to_string(), call[1].clone())
}
/// The ids of the roles with this name and tenant, as an administrator sees them.
async fn roles_named(admin: &Account, description: &str, tenant: Option<Id>) -> Vec<Id> {
let mut found = Vec::new();
for id in admin
.registry_query_ids(ObjectType::Role, Vec::<(&str, &str)>::new(), Vec::<&str>::new())
.await
{
let role = admin.registry_get::<Role>(id).await;
if role.description == description && role.member_tenant_id == tenant {
found.push(id);
}
}
found
}
pub async fn test(test: &mut TestServer) {
println!("Running compliance role tests...");
let admin = test.account("[email protected]");
// The server's officer role exists, once
let officer_role = roles_named(&admin, "Compliance Officer", None).await;
let user_role = roles_named(&admin, "User", None).await;
assert_eq!(officer_role.len(), 1, "one server-level Compliance Officer role");
assert_eq!(user_role.len(), 1);
// A compliance officer: the role carries a user's own permissions too
let officer = admin
.create_user_account("[email protected]", "officer-secret-4410", "Officer", &[], vec![])
.await;
admin
.registry_update_object(
ObjectType::Account,
officer.id(),
json!({Property::Roles: UserRoles::Custom(CustomRoles {
role_ids: Map::new(vec![officer_role[0]]),
})}),
)
.await;
// Reads and exports the audit log
let (name, response) = call(&officer, "inbuxa:AuditEvent/query", json!({})).await;
assert_eq!(name, "inbuxa:AuditEvent/query", "the officer reads the audit log: {response}");
// Places and releases a hold: that is the role
let (name, response) = call(
&officer,
"inbuxa:LegalHold/set",
json!({"reason": "Regulator's request", "create": {"h": {"name": "Matter 9001",
"scope": {"accounts": [officer.id_string()]}}}}),
)
.await;
let hold = response["created"]["h"]["id"]
.as_str()
.unwrap_or_else(|| panic!("the officer places a hold: {name} {response}"))
.to_string();
let (_, response) = call(
&officer,
"inbuxa:LegalHold/set",
json!({"reason": "Closed", "update": {hold.as_str(): {"released": true}}}),
)
.await;
assert!(
response["updated"].get(hold.as_str()).is_some(),
"the officer releases a hold: {response}"
);
// Changes no server setting and creates no account
let (name, response) = call(
&officer,
"x:DataRetention/set",
json!({"update": {"singleton": {"holdTracesFor": 86400000}}}),
)
.await;
assert!(
name == "error" || response["notUpdated"].get("singleton").is_some(),
"the officer changed a setting: {name} {response}"
);
let (name, response) = call(
&officer,
"x:Account/set",
json!({"create": {"a": {"@type": "User", "name": "nobody"}}}),
)
.await;
assert!(
name == "error" || response["notCreated"].get("a").is_some(),
"the officer created an account: {name} {response}"
);
let (name, response) = call(
&officer,
"inbuxa:AuditSettings/set",
json!({"update": {"singleton": {"keepForDays": 90}}}),
)
.await;
assert!(
name == "error" || response["notUpdated"].get("singleton").is_some(),
"the officer shortened audit retention: {name} {response}"
);
// A tenant compliance officer reads its tenant's audit log, and no holds
let tenant = admin
.registry_create_object(Tenant {
name: "compliance-tenant".to_string(),
..Default::default()
})
.await;
admin
.registry_create_object(Domain {
name: "tenant-compliance.example.org".to_string(),
is_enabled: true,
member_tenant_id: Some(tenant),
certificate_management: CertificateManagement::Manual,
dns_management: DnsManagement::Manual,
dkim_management: DkimManagement::Manual,
..Default::default()
})
.await;
// A new tenant gets its own Compliance Officer role (MT-3: a tenant's
// accounts hold only its own roles)
let tenant_role = roles_named(&admin, "Compliance Officer", Some(tenant)).await;
assert_eq!(tenant_role.len(), 1, "the tenant's Compliance Officer role");
let t_officer = admin
.create_user_account(
"[email protected]",
"tenant-officer-secret-7715",
"Tenant officer",
&[],
vec![],
)
.await;
admin
.registry_update_object(
ObjectType::Account,
t_officer.id(),
json!({Property::Roles: UserRoles::Custom(CustomRoles {
role_ids: Map::new(vec![tenant_role[0]]),
})}),
)
.await;
let (name, response) = call(&t_officer, "inbuxa:AuditEvent/query", json!({})).await;
assert_eq!(name, "inbuxa:AuditEvent/query", "the tenant officer reads the audit log: {response}");
let (name, response) = call(&t_officer, "inbuxa:LegalHold/get", json!({"ids": null})).await;
assert_eq!(name, "error", "LH-13: the tenant officer read holds: {response}");
// A tenant can still be deleted: its unused role goes with it
let spare = admin
.registry_create_object(Tenant {
name: "spare-tenant".to_string(),
..Default::default()
})
.await;
assert_eq!(roles_named(&admin, "Compliance Officer", Some(spare)).await.len(), 1);
let (name, response) = call(&admin, "x:Tenant/set", json!({"destroy": [spare.to_string()]})).await;
assert!(
response["destroyed"].as_array().is_some_and(|d| d.iter().any(|i| i == &json!(spare.to_string()))),
"the tenant was deleted: {name} {response}"
);
assert!(roles_named(&admin, "Compliance Officer", Some(spare)).await.is_empty());
}
#[ignore]
#[tokio::test(flavor = "multi_thread")]
pub async fn compliance_tests() {
let mut test = TestServerBuilder::new("compliance_tests")
.await
.with_default_listeners()
.await
.build()
.await;
let admin = test.create_admin_account("[email protected]").await;
test.insert_account(admin);
self::test(&mut test).await;
if test.is_reset() {
test.temp_dir.delete();
}
}
+1
View File
@@ -13,6 +13,7 @@ pub mod ai_calibration;
pub mod ai_explain; pub mod ai_explain;
pub mod account_lock; // inbuxa: account lock with delegation pub mod account_lock; // inbuxa: account lock with delegation
pub mod legal_hold; // inbuxa: legal hold pub mod legal_hold; // inbuxa: legal hold
pub mod compliance; // inbuxa: the compliance roles
pub mod audit; // inbuxa: the audit log pub mod audit; // inbuxa: the audit log
pub mod authorization; pub mod authorization;
pub mod auto_reload; // inbuxa: registry writes apply at once pub mod auto_reload; // inbuxa: registry writes apply at once