Enterprise permissions and ordinary CRUD
Maya starts using the writing app on her own. Her posts and documents are private. Later, she creates Acme and invites Ken to help with Finance and Eve to work in Engineering. Ken should see the Finance budget, but joining the same company shouldn't reveal Engineering's documents.
You can model that with an organization, a few workspaces, and relationships between people and resources. This example is one policy you can build with Rent—not a set of enterprise objects every Rent application has to adopt.
Start with the places people work
Think of the organization as the tenant boundary, and a workspace as a place to share resources with a particular set of people:
Organization
├── Finance workspace
│ ├── Budget document
│ └── Forecast document
└── Engineering workspace
└── Design documentA personal organization uses the same idea with one initial member. Your application can create its initial scope during onboarding so someone using the app alone doesn't have to configure an enterprise hierarchy first.
Joining the organization doesn't automatically grant every workspace's content. In this policy, even organization administration and document access are separate choices.
Let documents use their containing scope
A document's scope relationship says where its access comes from:
permissions Document {
read = scope.viewer
create = scope.creator_permission
update = scope.editor
delete = scope.deleter
}Read this as “people who can view this scope can read this document.” The other lines make the same connection for creating, editing, and deleting.
scope, viewer, creator_permission, editor, and deleter are definitions in this
application. Here, a model named ResourceScope represents a workspace or a more
specific sharing boundary. Documents and other resource types can point to it.
Share the Finance budget
Give Ken an Editor role on the Budget scope. In this example, a role assignment is an ordinary model connecting a principal, role, and scope:
request_client.create_member_binding()
.scope_id("budget")
.role_id("editor")
.principal_id("ken")
.save()
.await?;The sharing endpoint uses the current caller's scoped client. Its rules require sharing authority and prevent giving away capabilities the caller doesn't have.
Ken can edit Budget. That doesn't make him an editor of Forecast, or a member of Engineering, unless another assignment grants those routes. To remove this assignment:
request_client.delete_member_binding()
.scope_id_eq("budget")
.role_id_eq("editor")
.principal_id_eq("ken")
.exec()
.await?;No RSL edit or new JWT is needed. Roles and grants are application data. You can also share with a group and let group membership decide which people benefit.
Keep document handlers ordinary
After authenticating a request and checking the selected organization, bind its principal and tenant once:
let request_client = base.clone().with_viewer(
Viewer::of(Principal::subject(PrincipalId {
tenant_id: verified_tenant,
id: verified_local_principal,
}))
.tenant(tenant_context),
);A principal is the person or service account acting inside this tenant. Those values come from your trusted authentication and membership lookup.
The handler still uses ordinary generated queries and updates:
let documents = request_client.document()
.order_by(Document::id)
.limit(25)
.all()
.await?;
let changed = request_client.update_document()
.one(document_id)
.body(new_body)
.save_one()
.await?;The list returns documents this person can read. Editing checks their update rule. The handler doesn't need to reconstruct the workspace's role hierarchy each time. See scoped clients for service boundaries.
Reuse the policy across resource types
A Guild, Document, and another application resource can stay distinct models while sharing the same common policy:
permissions profile ScopedResource {
read = scope.viewer
create = scope.creator_permission
update = scope.editor
delete = scope.deleter
}
permissions Document {
use ScopedResource
}
permissions Guild {
use ScopedResource
}Both models declare a scope relationship. A
model mixin can share those fields too. A supporting
record such as Comment can instead follow its parent Document's rule, so it needs
no separate sharing screen.
Give a resource its own scope only when it needs independent sharing. Keeping several resources in one scope is useful when they should always follow the same audience.
Add a rule for a particular workflow
Suppose a document needs approval, but authors shouldn't approve their own work:
permissions Document {
use ScopedResource
approver = scope.approver but not creator
approve = approver
}creator is the document's creator relationship. but not excludes that person from
the scope's approvers. Other resource types keep their simpler shared policy.
Choose roles for the work people do
This example separates several responsibilities:
| Person | Responsibility |
|---|---|
| Organization owner | Manages organization ownership and membership |
| Security administrator | Manages groups, role definitions, and access administration |
| Workspace editor | Changes resources in their assigned scope |
| Workspace reader | Reads resources in their assigned scope |
| Guest | Receives a deliberately limited resource assignment |
| Service account | Acts with its own assigned permissions |
Roles can combine, but active organization membership is still required. Custom roles store the application's capability choices as data, so changing assignments doesn't require new code. Adding an entirely new capability does involve designing that capability in the schema and application.
A resource's creator and owner need not always be the same. Creating a document in a shared workspace can leave control with the workspace. When a product wants “the creator owns this new resource,” its create command explicitly establishes that ownership along with the resource.
Use identities in credentials, keep access in data
A JWT identifies the signed-in subject and session. The application verifies it. Membership, roles, and resource assignments live in the database. A service-account API key identifies that account and can place an additional ceiling on what the key may do. A read-only key must not gain write access simply because its account can edit.
Removing membership makes surviving grants ineffective in this policy. Rejoining with the same stable identity can make those grants apply again. Ordinary reads and writes use the current facts visible to their transaction.
Keep business commands together
Ownership transfer and membership removal often change several records. Keep those steps in an application transaction. For example, protecting the last active owner needs an application command that checks the invariant with appropriate locking. Field permissions can handle narrower self-service changes without turning permission rules into general business validation.
Similarly, a document, first comment, and audit event can commit together. Keep privileged audit writing in a narrow trusted adapter. Ordinary content handlers receive scoped clients. See trusted adapters.
For a smaller starting point, use private ownership and sharing. For efficient list pages and commands, continue with production access patterns.
Complete example
Download this project, or copy the files below into an empty directory.
Replace RENT_REPOSITORY_URL in Cargo.toml with the URL supplied with your preview access. Use the matching Rent CLI release.
Use a separate local database for this example:
export DATABASE_URL='sqlite://lesson.db?mode=rwc'
rent generate
rent migrate dev --name initial
cargo runRun the included test with cargo nextest run. It creates an isolated temporary database.
Cargo.toml
[package]
name = "rent-lesson-37-enterprise-permissions"
version = "0.1.0"
edition = "2024"
[workspace]
[[bin]]
name = "rent-lesson-37-enterprise-permissions"
path = "main.rs"
[dependencies]
rent = { git = "RENT_REPOSITORY_URL", tag = "v0.7.2", default-features = false, features = ["sqlite", "migrate", "extensions"] }
anyhow = "1.0.104"
tokio = { version = "1.53.1", features = ["full"] }
tempfile = "3.27.0"rent.toml
schema = "schema.rsl"
generated = "generated"
migrations = "migrations"
[database]
dialect = "sqlite"
backends = ["sqlite"]schema.rsl
// A reusable application permission model. All names except RSL keywords are ours.
datasource db {
provider = "sqlite"
}
tenancy {
scope = "required"
field = "tenant_id"
}
access {
subjects = [Principal]
}
enum PrincipalKind {
HUMAN
SERVICE
}
enum MemberRole {
OWNER
SECURITY_ADMIN
BILLING_ADMIN
MEMBER
GUEST
SERVICE
}
model Organization {
tenant_id String @id @rent.type(mysql: "varchar(64)")
name String
memberships Membership[]
principals Principal[]
groups AccessGroup[]
roles Role[]
scopes ResourceScope[]
}
model Principal {
tenant_id String @rent.type(mysql: "varchar(64)")
id String @rent.type(mysql: "varchar(64)")
name String
kind PrincipalKind @default(HUMAN)
enabled Boolean @default(true)
organization Organization @relation(fields: [tenant_id], references: [tenant_id])
memberships Membership[]
groups GroupMember[]
bindings MemberBinding[]
@@id([tenant_id, id])
}
model Membership {
tenant_id String @rent.type(mysql: "varchar(64)")
principal_id String @rent.type(mysql: "varchar(64)")
role MemberRole @default(MEMBER)
active Boolean @default(true)
organization Organization @relation(fields: [tenant_id], references: [tenant_id])
principal Principal @relation(fields: [tenant_id, principal_id], references: [tenant_id, id])
@@id([tenant_id, principal_id])
}
model AccessGroup {
tenant_id String @rent.type(mysql: "varchar(64)")
id String @rent.type(mysql: "varchar(64)")
name String
organization Organization @relation(fields: [tenant_id], references: [tenant_id])
members GroupMember[]
bindings GroupBinding[]
@@id([tenant_id, id])
}
model GroupMember {
tenant_id String @rent.type(mysql: "varchar(64)")
group_id String @rent.type(mysql: "varchar(64)")
principal_id String @rent.type(mysql: "varchar(64)")
group AccessGroup @relation(fields: [tenant_id, group_id], references: [tenant_id, id])
principal Principal @relation(fields: [tenant_id, principal_id], references: [tenant_id, id])
@@id([tenant_id, group_id, principal_id])
}
// Custom roles are data: editing these flags requires no schema generation.
// The application owns the finite action catalog, not customer-supplied policy code.
model Role {
tenant_id String @rent.type(mysql: "varchar(64)")
id String @rent.type(mysql: "varchar(64)")
name String
can_read Boolean @default(false)
can_edit Boolean @default(false)
can_comment Boolean @default(false)
can_delete Boolean @default(false)
can_share Boolean @default(false)
can_create Boolean @default(false)
can_approve Boolean @default(false)
organization Organization @relation(fields: [tenant_id], references: [tenant_id])
people MemberBinding[]
groups GroupBinding[]
@@id([tenant_id, id])
}
// Security settings live here, away from fields an ordinary content editor can change.
model ResourceScope {
tenant_id String @rent.type(mysql: "varchar(64)")
id String @rent.type(mysql: "varchar(64)")
name String
parent_id String? @rent.immutable @rent.type(mysql: "varchar(64)")
owner_id String @rent.type(mysql: "varchar(64)")
created_by String @rent.type(mysql: "varchar(64)")
inherit_access Boolean @default(true)
organization Organization @relation(fields: [tenant_id], references: [tenant_id])
parent ResourceScope? @relation("ScopeTree", fields: [tenant_id, parent_id], references: [tenant_id, id])
children ResourceScope[] @relation("ScopeTree")
owner_subject Principal @relation("ScopeOwner", fields: [tenant_id, owner_id], references: [tenant_id, id])
creator Principal @relation("ScopeCreator", fields: [tenant_id, created_by], references: [tenant_id, id]) @rent.creator
people MemberBinding[]
groups GroupBinding[]
documents Document[]
guilds Guild[]
@@id([tenant_id, id])
}
model MemberBinding {
tenant_id String @rent.type(mysql: "varchar(64)")
scope_id String @rent.immutable @rent.type(mysql: "varchar(64)")
role_id String @rent.immutable @rent.type(mysql: "varchar(64)")
principal_id String @rent.immutable @rent.type(mysql: "varchar(64)")
expires_at DateTime? @rent.expires
scope ResourceScope @relation(fields: [tenant_id, scope_id], references: [tenant_id, id], onDelete: Cascade)
role Role @relation(fields: [tenant_id, role_id], references: [tenant_id, id])
principal Principal @relation(fields: [tenant_id, principal_id], references: [tenant_id, id])
@@id([tenant_id, scope_id, role_id, principal_id])
}
model GroupBinding {
tenant_id String @rent.type(mysql: "varchar(64)")
scope_id String @rent.immutable @rent.type(mysql: "varchar(64)")
role_id String @rent.immutable @rent.type(mysql: "varchar(64)")
group_id String @rent.immutable @rent.type(mysql: "varchar(64)")
expires_at DateTime? @rent.expires
scope ResourceScope @relation(fields: [tenant_id, scope_id], references: [tenant_id, id], onDelete: Cascade)
role Role @relation(fields: [tenant_id, role_id], references: [tenant_id, id])
group AccessGroup @relation(fields: [tenant_id, group_id], references: [tenant_id, id])
@@id([tenant_id, scope_id, role_id, group_id])
}
// Structured application data is independent of the permission rules.
type DocumentAppearance {
theme String
show_outline Boolean
page_width Int
}
type DocumentNotifications {
enabled Boolean
channels String[]
digest String
}
type DocumentRetention {
archive_after_days Int
keep_revisions Boolean
legal_hold Boolean
}
type DocumentSettings {
appearance DocumentAppearance
notifications DocumentNotifications
retention DocumentRetention
locale String
custom Json?
}
// A storage declaration shared by independently permissioned resource types.
// It creates no table; each receiving model gets ordinary fields and relations.
mixin TenantKey {
tenant_id String @rent.type(mysql: "varchar(64)")
}
mixin ScopedResourceFields {
use TenantKey
id String @rent.type(mysql: "varchar(64)")
scope_id String @rent.immutable @rent.type(mysql: "varchar(64)")
created_by String @rent.type(mysql: "varchar(64)")
scope ResourceScope @relation(fields: [tenant_id, scope_id], references: [tenant_id, id])
creator Principal @relation(fields: [tenant_id, created_by], references: [tenant_id, id]) @rent.creator
@@id([tenant_id, id])
}
model Document {
use ScopedResourceFields
title String
body String @default("") @rent.type(mysql: "text")
summary String?
tags String[] @default([])
metadata Json @default("{}")
settings DocumentSettings?
published_at DateTime?
archived Boolean @default(false)
comments Comment[]
approvals Approval[]
}
model Guild {
use ScopedResourceFields
name String
}
model Comment {
tenant_id String @rent.type(mysql: "varchar(64)")
id String @rent.type(mysql: "varchar(64)")
document_id String @rent.immutable @rent.type(mysql: "varchar(64)")
created_by String @rent.type(mysql: "varchar(64)")
body String
document Document @relation(fields: [tenant_id, document_id], references: [tenant_id, id], onDelete: Cascade)
creator Principal @relation(fields: [tenant_id, created_by], references: [tenant_id, id]) @rent.creator
@@id([tenant_id, id])
}
model AuditEvent {
tenant_id String @rent.type(mysql: "varchar(64)")
id String @rent.type(mysql: "varchar(64)")
actor_id String
action String
target_id String
organization Organization @relation(fields: [tenant_id], references: [tenant_id])
@@id([tenant_id, id])
}
permissions Organization {
member = memberships(active: true).principal(enabled: true)
owner = memberships(active: true, role: OWNER).principal(enabled: true)
security_admin = owner or memberships(active: true, role: SECURITY_ADMIN).principal(enabled: true)
billing_admin = owner or memberships(active: true, role: BILLING_ADMIN).principal(enabled: true)
contributor = memberships(active: true, role: [OWNER, SECURITY_ADMIN, MEMBER]).principal(enabled: true, kind: HUMAN)
read = member
create = nobody
update = security_admin
delete = owner
}
permissions Principal {
read = organization.member
create, update, delete = nobody
}
permissions Membership {
read = organization.security_admin or principal
create, update, delete = nobody
// Owners must use the lifecycle command, which protects the last active owner.
update(active) = principal(enabled: true)
and organization.member when role: [SECURITY_ADMIN, BILLING_ADMIN, MEMBER, GUEST, SERVICE]
update_to(active) = principal(enabled: true) when active: false
}
permissions AccessGroup {
member = members.principal(enabled: true) and organization.member
read = organization.member
create, update, delete = organization.security_admin
}
permissions GroupMember {
read = group.organization.member
create, delete = group.organization.security_admin
update = nobody
}
permissions Role {
allows_read = public when can_read: true
allows_edit = public when can_edit: true
allows_comment = public when can_comment: true
allows_delete = public when can_delete: true
allows_share = public when can_share: true
allows_create = public when can_create: true
allows_approve = public when can_approve: true
no_read = public when can_read: false
no_edit = public when can_edit: false
no_comment = public when can_comment: false
no_delete = public when can_delete: false
no_share = public when can_share: false
no_create = public when can_create: false
no_approve = public when can_approve: false
read = organization.member
create, update, delete = organization.security_admin
}
permissions ResourceScope {
owner = owner_subject(enabled: true) and organization.member
inherited_viewer = parent.viewer when inherit_access: true
inherited_editor = parent.editor when inherit_access: true
inherited_commenter = parent.commenter when inherit_access: true
inherited_deleter = parent.deleter when inherit_access: true
inherited_sharer = parent.sharer when inherit_access: true
inherited_creator = parent.creator_permission when inherit_access: true
inherited_approver = parent.approver when inherit_access: true
editor = (owner or people.editor or groups.editor or inherited_editor)
and organization.member
commenter = (editor or people.commenter or groups.commenter or inherited_commenter)
and organization.member
viewer = (commenter or people.viewer or groups.viewer or inherited_viewer)
and organization.member
deleter = (owner or people.deleter or groups.deleter or inherited_deleter)
and organization.member
sharer = (owner or people.sharer or groups.sharer or inherited_sharer)
and organization.member
creator_permission = (owner or people.creator_permission or groups.creator_permission or inherited_creator)
and organization.member
approver = (owner or people.approver or groups.approver or inherited_approver)
and organization.member
read = viewer
create = owner_subject
and organization.contributor
and (parent.creator_permission or public when parent_id: null)
update = owner or organization.security_admin
delete = owner
}
permissions MemberBinding {
viewer = principal(enabled: true) and role.allows_read
editor = principal(enabled: true) and role.allows_edit
commenter = principal(enabled: true) and role.allows_comment
deleter = principal(enabled: true) and role.allows_delete
sharer = principal(enabled: true) and role.allows_share
creator_permission = principal(enabled: true) and role.allows_create
approver = principal(enabled: true) and role.allows_approve
grantable = (scope.viewer or role.no_read)
and (scope.editor or role.no_edit)
and (scope.commenter or role.no_comment)
and (scope.deleter or role.no_delete)
and (scope.sharer or role.no_share)
and (scope.creator_permission or role.no_create)
and (scope.approver or role.no_approve)
read = scope.sharer or principal
create = scope.sharer and grantable
update, delete = scope.sharer
}
permissions GroupBinding {
viewer = group.member and role.allows_read
editor = group.member and role.allows_edit
commenter = group.member and role.allows_comment
deleter = group.member and role.allows_delete
sharer = group.member and role.allows_share
creator_permission = group.member and role.allows_create
approver = group.member and role.allows_approve
grantable = (scope.viewer or role.no_read)
and (scope.editor or role.no_edit)
and (scope.commenter or role.no_comment)
and (scope.deleter or role.no_delete)
and (scope.sharer or role.no_share)
and (scope.creator_permission or role.no_create)
and (scope.approver or role.no_approve)
read = scope.sharer or group.member
create = scope.sharer and grantable
update, delete = scope.sharer
}
// Shared by every resource that follows its containing scope.
permissions profile ScopedResource {
read = scope.viewer
create = scope.creator_permission
update = scope.editor
delete = scope.deleter
}
permissions Document {
use ScopedResource
approver = scope.approver but not creator
approve = approver
}
permissions Guild {
use ScopedResource
}
permissions Comment {
read = document.scope.viewer
create = document.scope.commenter
update = creator and document.scope.commenter
delete = creator and document.scope.commenter or document.scope.deleter
}
permissions AuditEvent {
read = organization.security_admin
create, update, delete = nobody
}
// A business action is an authorized, append-only fact, not a client-editable flag.
model Approval {
tenant_id String @rent.type(mysql: "varchar(64)")
document_id String @rent.type(mysql: "varchar(64)")
reviewer_id String @rent.type(mysql: "varchar(64)")
document Document @relation(fields: [tenant_id, document_id], references: [tenant_id, id])
reviewer Principal @relation(fields: [tenant_id, reviewer_id], references: [tenant_id, id]) @rent.creator
@@id([tenant_id, document_id, reviewer_id])
}
permissions Approval {
read = document.scope.viewer
create = document.approver
update, delete = nobody
}main.rs
use anyhow::{Result, ensure};
use rent::policy::viewer::Viewer;
#[allow(dead_code, unused_imports)]
#[rustfmt::skip]
#[path = "generated/mod.rs"]
mod generated;
use generated::client::Client;
use generated::{
client::ScopedClient,
document::{Document, DocumentId, DocumentPermission},
principal::{Principal, PrincipalId},
};
mod seed;
fn actor(
root: &Client,
tenant: &str,
user: &str,
) -> ScopedClient {
root.clone().with_viewer(
Viewer::of(Principal::subject(PrincipalId {
tenant_id: tenant.into(),
id: user.into(),
}))
.tenant(tenant),
)
}
async fn example(url: &str) -> Result<()> {
let root = Client::connect(url).await?;
seed::run(&root).await?;
let maya = actor(&root, "acme", "maya");
let ken = actor(&root, "acme", "ken");
let eve = actor(&root, "acme", "eve");
let budget = DocumentId {
tenant_id: "acme".into(),
id: "budget".into(),
};
ensure!(ken.document_permissions(&budget).can(DocumentPermission::Update).await?);
ensure!(!eve.document_permissions(&budget).can(DocumentPermission::Read).await?);
maya.create_member_binding()
.scope_id("budget")
.role_id("viewer")
.principal_id("eve")
.save()
.await?;
ensure!(eve.document_permissions(&budget).can(DocumentPermission::Read).await?);
ensure!(!eve.document_permissions(&budget).can(DocumentPermission::Update).await?);
let rows = ken.document().order_by(Document::id).all().await?;
ensure!(rows.iter().all(|row| row.tenant_id == "acme"));
ensure!(rows.iter().any(|row| row.id == "budget"));
ensure!(!rows.iter().any(|row| row.id == "engineering"));
println!("Ken can edit Finance's budget. Eve can read only the budget shared with her.");
Ok(())
}
#[tokio::main]
async fn main() -> Result<()> {
example(&std::env::var("DATABASE_URL")?).await
}
#[cfg(test)]
mod test_support;
#[cfg(test)]
mod tests {
#[tokio::test]
async fn example_runs() -> anyhow::Result<()> {
let (_directory, url) = super::test_support::database()?;
super::example(&url).await
}
}seed.rs
use crate::generated::enums::{MemberRole, PrincipalKind};
use crate::{Client, actor};
use anyhow::Result;
#[allow(clippy::too_many_lines)]
pub(super) async fn run(base: &Client) -> Result<()> {
for tenant in ["acme", "northwind", "personal"] {
let system = base.system();
system.create_organization().tenant_id(tenant).name(tenant).save().await?;
for (id, role, kind, active) in [
("maya", MemberRole::Owner, PrincipalKind::Human, true),
("sana", MemberRole::SecurityAdmin, PrincipalKind::Human, true),
("bill", MemberRole::BillingAdmin, PrincipalKind::Human, true),
("ken", MemberRole::Member, PrincipalKind::Human, true),
("jo", MemberRole::Member, PrincipalKind::Human, true),
("eve", MemberRole::Member, PrincipalKind::Human, true),
("gia", MemberRole::Guest, PrincipalKind::Human, true),
("scout", MemberRole::Service, PrincipalKind::Service, true),
("sam", MemberRole::Member, PrincipalKind::Human, false),
] {
if tenant == "personal" && id != "maya" {
continue;
}
system
.create_principal()
.tenant_id(tenant)
.id(id)
.name(id)
.kind(kind)
.save()
.await?;
system
.create_membership()
.tenant_id(tenant)
.principal_id(id)
.role(role)
.active(active)
.save()
.await?;
}
let maya = actor(base, tenant, "maya");
for (id, read, edit, comment, delete, share, create, approve) in [
("viewer", true, false, false, false, false, false, false),
("editor", true, true, true, false, false, true, false),
("commenter", true, false, true, false, false, false, false),
("sharer", true, false, false, false, true, false, false),
("approver", true, false, false, false, false, false, true),
("manager", true, true, true, true, true, true, true),
] {
maya.create_role()
.id(id)
.name(id)
.can_read(read)
.can_edit(edit)
.can_comment(comment)
.can_delete(delete)
.can_share(share)
.can_create(create)
.can_approve(approve)
.save()
.await?;
}
maya.create_resource_scope()
.id("finance")
.name("Finance")
.owner_id("maya")
.save()
.await?;
maya.create_resource_scope()
.id("budget")
.name("Budget")
.parent_id("finance")
.owner_id("maya")
.save()
.await?;
maya.create_resource_scope()
.id("secret")
.name("Restricted audit")
.parent_id("finance")
.owner_id("maya")
.inherit_access(false)
.save()
.await?;
maya.create_document()
.id("budget")
.scope_id("budget")
.title("Budget")
.body("original")
.save()
.await?;
maya.create_document()
.id("secret")
.scope_id("secret")
.title("Restricted")
.body("private")
.save()
.await?;
if tenant == "personal" {
continue;
}
actor(base, tenant, "eve")
.create_resource_scope()
.id("engineering")
.name("Engineering")
.owner_id("eve")
.save()
.await?;
actor(base, tenant, "eve")
.create_document()
.id("engineering")
.scope_id("engineering")
.title("Engineering")
.save()
.await?;
maya.create_access_group().id("finance").name("Finance staff").save().await?;
maya.create_group_member()
.group_id("finance")
.principal_id("ken")
.save()
.await?;
maya.create_group_binding()
.scope_id("finance")
.group_id("finance")
.role_id("editor")
.save()
.await?;
for (id, target) in [
("jo", "finance"),
("scout", "finance"),
("sam", "finance"),
("gia", "secret"),
] {
maya.create_member_binding()
.scope_id(target)
.role_id("viewer")
.principal_id(id)
.save()
.await?;
}
}
Ok(())
}test_support.rs
use anyhow::{Context, Result, ensure};
use std::process::Command;
pub fn database() -> Result<(tempfile::TempDir, String)> {
let directory = tempfile::tempdir()?;
std::fs::write(directory.path().join("schema.rsl"), include_str!("schema.rsl"))?;
std::fs::write(directory.path().join("rent.toml"), r#"schema = "schema.rsl""#)?;
let url = format!("sqlite://{}?mode=rwc", directory.path().join("app.db").display());
let executable = std::env::var("RENT_CLI").unwrap_or_else(|_| "rent".into());
let output = Command::new(executable)
.arg("migrate")
.arg("dev")
.arg("--root")
.arg(directory.path())
.arg("--to")
.arg("schema.rsl")
.arg("--database-url")
.arg(&url)
.output()
.context("run the Rent CLI")?;
ensure!(
output.status.success(),
"migration failed: {}",
String::from_utf8_lossy(&output.stderr)
);
Ok((directory, url))
}