Skip to main content

apiplant_core/
defaults.rs

1//! Built-in resources.
2//!
3//! `organization`, `membership`, `user`, `api_key` and `oauth_connection` exist
4//! in every app. Together they make apps multitenant out of the box: users join
5//! organisations through memberships (which also carry their **role within that
6//! organisation**), and every other resource is isolated per organisation by
7//! default.
8//!
9//! Drop a `models/<name>.toml` with the same `name` to replace a built-in and
10//! add fields or tweak permissions while keeping the machinery working.
11
12use crate::schema::Resource;
13
14/// The `organization` — the tenant. `global` because its rows *are* the
15/// organisations; membership (not an `organization_id`) decides who sees them.
16pub const ORGANIZATION_TOML: &str = r#"
17[resource]
18name = "organization"
19scope = "global"
20timestamps = true
21
22[permissions]
23list   = "member"          # organisations you belong to
24read   = "member"
25create = "authenticated"   # anyone may start one (and becomes its admin)
26update = "role:admin"      # an admin *of that organisation*
27delete = "role:admin"
28
29[fields.name]
30type = "string"
31required = true
32
33[fields.slug]
34type = "string"
35unique = true
36"#;
37
38/// A user's membership in an organisation, carrying their role there. The
39/// join table behind the N:N between `user` and `organization`.
40pub const MEMBERSHIP_TOML: &str = r#"
41[resource]
42name = "membership"
43scope = "organization"
44timestamps = true
45
46[permissions]
47list   = "member"          # members can see who else is in the org
48read   = "member"
49create = "role:admin"      # admins add members
50update = "role:admin"
51delete = "role:admin"
52
53# Built-in function: lets `create` name the person by `email` instead of by
54# `user_id`, and refuses a duplicate membership. The lookup has to happen here
55# because `user` is only readable by people you already share an org with.
56[hooks]
57before_create = "apiplant_organization_join"
58
59[fields.user_id]
60type = "reference"
61references = "user"
62required = true
63
64[fields.organization_id]
65type = "reference"
66references = "organization"
67required = true
68
69[fields.role]
70type = "string"            # the member's *primary* role here, e.g. "admin".
71                           # Further roles are `membership_role` rows; a
72                           # `role:` permission is checked against all of them.
73"#;
74
75/// A single role held by a membership.
76///
77/// Roles are a set, not a slot: someone can be a `billing` *and* a `support`
78/// person without either displacing the other, which one column cannot express.
79/// [`MEMBERSHIP_TOML`]'s `role` stays as the member's **primary** role — it is
80/// what existing apps and hook contexts read — and these rows are the rest.
81/// Together they are the roles a `role:` permission is checked against.
82///
83/// `admin` is special: it satisfies every role check in its organisation
84/// without needing a row per role, so granting someone `admin` grants them
85/// everything the app defines.
86pub const MEMBERSHIP_ROLE_TOML: &str = r#"
87[resource]
88name = "membership_role"
89scope = "organization"
90timestamps = true
91
92[permissions]
93list   = "member"          # members can see who holds what
94read   = "member"
95create = "role:admin"      # admins grant roles
96update = "role:admin"
97delete = "role:admin"
98
99[fields.membership_id]
100type = "reference"
101references = "membership"
102required = true
103on_delete = "cascade"      # a removed member takes their roles with them
104
105[fields.organization_id]
106type = "reference"
107references = "organization"
108required = true
109
110[fields.role]
111type = "string"
112required = true
113"#;
114
115/// The default `user`: global (users are shared across organisations) with
116/// email + password auth. Extend via `models/users.toml`.
117pub const USER_TOML: &str = r#"
118[resource]
119name = "user"
120scope = "global"
121timestamps = true
122
123[permissions]
124list   = "member"          # people you share an organisation with
125read   = "member"
126create = "public"          # registration
127update = "owner"
128delete = "private"
129
130[auth]
131identity_field = "email"
132password_field = "password_hash"
133oauth_providers = []
134
135[fields.email]
136type = "string"
137required = true
138unique = true
139max_length = 320
140
141[fields.password_hash]
142type = "string"
143hidden = true
144
145[fields.display_name]
146type = "string"
147"#;
148
149/// Default `api_key` resource (global). A valid key authenticates as its owner.
150pub const API_KEY_TOML: &str = r#"
151[resource]
152name = "api_key"
153scope = "global"
154timestamps = true
155
156# "Api key" is what titleising the name produces, and it is not how anybody
157# writes it.
158[admin]
159label = "API key"
160plural = "API keys"
161
162[permissions]
163list   = "owner"
164read   = "owner"
165create = "authenticated"
166update = "private"
167delete = "owner"
168
169[fields.name]
170type = "string"
171
172[fields.token_hash]
173type = "string"
174required = true
175unique = true
176hidden = true
177
178[fields.owner_id]
179type = "reference"
180references = "user"
181required = true
182"#;
183
184/// Default `oauth_connection` resource (global) linking a user to a provider.
185pub const OAUTH_TOML: &str = r#"
186[resource]
187name = "oauth_connection"
188scope = "global"
189timestamps = true
190
191[permissions]
192list   = "owner"
193read   = "owner"
194create = "private"
195update = "private"
196delete = "owner"
197
198[fields.provider]
199type = "string"
200required = true
201
202[fields.provider_user_id]
203type = "string"
204required = true
205
206[fields.owner_id]
207type = "reference"
208references = "user"
209required = true
210"#;
211
212/// The name → embedded-TOML table of built-ins, in dependency order so foreign
213/// keys resolve (organization and user before membership/api_key/oauth).
214pub fn builtins() -> Vec<(&'static str, &'static str)> {
215    vec![
216        ("organization", ORGANIZATION_TOML),
217        ("user", USER_TOML),
218        ("membership", MEMBERSHIP_TOML),
219        ("membership_role", MEMBERSHIP_ROLE_TOML),
220        ("api_key", API_KEY_TOML),
221        ("oauth_connection", OAUTH_TOML),
222    ]
223}
224
225/// Parse one built-in by its embedded TOML. Panics on a malformed built-in —
226/// that would be a bug in this crate, caught by the test below.
227pub fn parse_builtin(toml_src: &str) -> Resource {
228    let r: Resource = toml::from_str(toml_src).expect("built-in resource TOML is valid");
229    r.validate().expect("built-in resource is valid");
230    r
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn all_builtins_parse() {
239        for (name, src) in builtins() {
240            let r = parse_builtin(src);
241            assert_eq!(r.meta.name, name);
242        }
243    }
244}