1use std::collections::{BTreeMap, BTreeSet};
9
10use crate::manifest::{ExpandedManifest, Grant, ObjectType, Privilege, RoleDefinition};
11
12#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
18pub struct RoleState {
19 pub login: bool,
20 pub superuser: bool,
21 pub createdb: bool,
22 pub createrole: bool,
23 pub inherit: bool,
24 pub replication: bool,
25 pub bypassrls: bool,
26 pub connection_limit: i32,
27 pub comment: Option<String>,
28}
29
30impl Default for RoleState {
31 fn default() -> Self {
32 Self {
33 login: false,
34 superuser: false,
35 createdb: false,
36 createrole: false,
37 inherit: true, replication: false,
39 bypassrls: false,
40 connection_limit: -1, comment: None,
42 }
43 }
44}
45
46impl RoleState {
47 pub fn from_definition(definition: &RoleDefinition) -> Self {
50 let defaults = Self::default();
51 Self {
52 login: definition.login.unwrap_or(defaults.login),
53 superuser: definition.superuser.unwrap_or(defaults.superuser),
54 createdb: definition.createdb.unwrap_or(defaults.createdb),
55 createrole: definition.createrole.unwrap_or(defaults.createrole),
56 inherit: definition.inherit.unwrap_or(defaults.inherit),
57 replication: definition.replication.unwrap_or(defaults.replication),
58 bypassrls: definition.bypassrls.unwrap_or(defaults.bypassrls),
59 connection_limit: definition
60 .connection_limit
61 .unwrap_or(defaults.connection_limit),
62 comment: definition.comment.clone(),
63 }
64 }
65
66 pub fn changed_attributes(&self, other: &RoleState) -> Vec<RoleAttribute> {
68 let mut changes = Vec::new();
69 if self.login != other.login {
70 changes.push(RoleAttribute::Login(other.login));
71 }
72 if self.superuser != other.superuser {
73 changes.push(RoleAttribute::Superuser(other.superuser));
74 }
75 if self.createdb != other.createdb {
76 changes.push(RoleAttribute::Createdb(other.createdb));
77 }
78 if self.createrole != other.createrole {
79 changes.push(RoleAttribute::Createrole(other.createrole));
80 }
81 if self.inherit != other.inherit {
82 changes.push(RoleAttribute::Inherit(other.inherit));
83 }
84 if self.replication != other.replication {
85 changes.push(RoleAttribute::Replication(other.replication));
86 }
87 if self.bypassrls != other.bypassrls {
88 changes.push(RoleAttribute::Bypassrls(other.bypassrls));
89 }
90 if self.connection_limit != other.connection_limit {
91 changes.push(RoleAttribute::ConnectionLimit(other.connection_limit));
92 }
93 changes
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
99pub enum RoleAttribute {
100 Login(bool),
101 Superuser(bool),
102 Createdb(bool),
103 Createrole(bool),
104 Inherit(bool),
105 Replication(bool),
106 Bypassrls(bool),
107 ConnectionLimit(i32),
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)]
118pub struct GrantKey {
119 pub role: String,
121 pub object_type: ObjectType,
123 pub schema: Option<String>,
125 pub name: Option<String>,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
131pub struct GrantState {
132 pub privileges: BTreeSet<Privilege>,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)]
141pub struct DefaultPrivKey {
142 pub owner: String,
144 pub schema: String,
146 pub on_type: ObjectType,
148 pub grantee: String,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
154pub struct DefaultPrivState {
155 pub privileges: BTreeSet<Privilege>,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)]
164pub struct MembershipEdge {
165 pub role: String,
167 pub member: String,
169 pub inherit: bool,
171 pub admin: bool,
173}
174
175#[derive(Debug, Clone, Default)]
184pub struct RoleGraph {
185 pub roles: BTreeMap<String, RoleState>,
187 pub grants: BTreeMap<GrantKey, GrantState>,
189 pub default_privileges: BTreeMap<DefaultPrivKey, DefaultPrivState>,
191 pub memberships: BTreeSet<MembershipEdge>,
193}
194
195impl RoleGraph {
196 pub fn from_expanded(
201 expanded: &ExpandedManifest,
202 default_owner: Option<&str>,
203 ) -> Result<Self, crate::manifest::ManifestError> {
204 let mut graph = Self::default();
205
206 for role_def in &expanded.roles {
208 let state = RoleState::from_definition(role_def);
209 graph.roles.insert(role_def.name.clone(), state);
210 }
211
212 for grant in &expanded.grants {
214 let key = grant_key_from_manifest(grant);
215 let entry = graph.grants.entry(key).or_insert_with(|| GrantState {
216 privileges: BTreeSet::new(),
217 });
218 for privilege in &grant.privileges {
219 entry.privileges.insert(*privilege);
220 }
221 }
222
223 for default_priv in &expanded.default_privileges {
225 let owner = default_priv
226 .owner
227 .as_deref()
228 .or(default_owner)
229 .unwrap_or("postgres")
230 .to_string();
231
232 for grant in &default_priv.grant {
233 let grantee = grant.role.clone().ok_or_else(|| {
234 crate::manifest::ManifestError::MissingDefaultPrivilegeRole {
235 schema: default_priv.schema.clone(),
236 }
237 })?;
238
239 let key = DefaultPrivKey {
240 owner: owner.clone(),
241 schema: default_priv.schema.clone(),
242 on_type: grant.on_type,
243 grantee,
244 };
245
246 let entry =
247 graph
248 .default_privileges
249 .entry(key)
250 .or_insert_with(|| DefaultPrivState {
251 privileges: BTreeSet::new(),
252 });
253 for privilege in &grant.privileges {
254 entry.privileges.insert(*privilege);
255 }
256 }
257 }
258
259 for membership in &expanded.memberships {
261 for member_spec in &membership.members {
262 graph.memberships.insert(MembershipEdge {
263 role: membership.role.clone(),
264 member: member_spec.name.clone(),
265 inherit: member_spec.inherit,
266 admin: member_spec.admin,
267 });
268 }
269 }
270
271 Ok(graph)
272 }
273}
274
275fn grant_key_from_manifest(grant: &Grant) -> GrantKey {
280 GrantKey {
281 role: grant.role.clone(),
282 object_type: grant.on.object_type,
283 schema: grant.on.schema.clone(),
284 name: grant.on.name.clone(),
285 }
286}
287
288impl PartialOrd for ObjectType {
293 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
294 Some(self.cmp(other))
295 }
296}
297
298impl Ord for ObjectType {
299 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
300 self.to_string().cmp(&other.to_string())
301 }
302}
303
304impl PartialOrd for Privilege {
305 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
306 Some(self.cmp(other))
307 }
308}
309
310impl Ord for Privilege {
311 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
312 self.to_string().cmp(&other.to_string())
313 }
314}
315
316#[cfg(test)]
321mod tests {
322 use super::*;
323 use crate::manifest::{expand_manifest, parse_manifest};
324
325 #[test]
326 fn role_state_defaults_match_postgres() {
327 let state = RoleState::default();
328 assert!(!state.login);
329 assert!(!state.superuser);
330 assert!(!state.createdb);
331 assert!(!state.createrole);
332 assert!(state.inherit); assert!(!state.replication);
334 assert!(!state.bypassrls);
335 assert_eq!(state.connection_limit, -1);
336 }
337
338 #[test]
339 fn role_state_from_definition_applies_overrides() {
340 let definition = RoleDefinition {
341 name: "test".to_string(),
342 login: Some(true),
343 superuser: None,
344 createdb: Some(true),
345 createrole: None,
346 inherit: Some(false),
347 replication: None,
348 bypassrls: None,
349 connection_limit: Some(10),
350 comment: Some("test role".to_string()),
351 };
352 let state = RoleState::from_definition(&definition);
353 assert!(state.login);
354 assert!(!state.superuser); assert!(state.createdb);
356 assert!(!state.createrole); assert!(!state.inherit); assert_eq!(state.connection_limit, 10);
359 assert_eq!(state.comment, Some("test role".to_string()));
360 }
361
362 #[test]
363 fn changed_attributes_detects_differences() {
364 let current = RoleState::default();
365 let desired = RoleState {
366 login: true,
367 connection_limit: 5,
368 ..RoleState::default()
369 };
370 let changes = current.changed_attributes(&desired);
371 assert_eq!(changes.len(), 2);
372 assert!(changes.contains(&RoleAttribute::Login(true)));
373 assert!(changes.contains(&RoleAttribute::ConnectionLimit(5)));
374 }
375
376 #[test]
377 fn changed_attributes_empty_when_equal() {
378 let state = RoleState::default();
379 assert!(state.changed_attributes(&state.clone()).is_empty());
380 }
381
382 #[test]
383 fn role_graph_from_expanded_manifest() {
384 let yaml = r#"
385default_owner: app_owner
386
387profiles:
388 editor:
389 grants:
390 - privileges: [USAGE]
391 on: { type: schema }
392 - privileges: [SELECT, INSERT]
393 on: { type: table, name: "*" }
394 default_privileges:
395 - privileges: [SELECT, INSERT]
396 on_type: table
397
398schemas:
399 - name: inventory
400 profiles: [editor]
401
402roles:
403 - name: analytics
404 login: true
405
406memberships:
407 - role: inventory-editor
408 members:
409 - name: "user@example.com"
410 inherit: true
411"#;
412 let manifest = parse_manifest(yaml).unwrap();
413 let expanded = expand_manifest(&manifest).unwrap();
414 let graph = RoleGraph::from_expanded(&expanded, manifest.default_owner.as_deref()).unwrap();
415
416 assert_eq!(graph.roles.len(), 2);
418 assert!(graph.roles.contains_key("inventory-editor"));
419 assert!(graph.roles.contains_key("analytics"));
420
421 assert!(!graph.roles["inventory-editor"].login);
423 assert!(graph.roles["analytics"].login);
424
425 assert_eq!(graph.grants.len(), 2);
427
428 assert_eq!(graph.default_privileges.len(), 1);
430 let dp_key = graph.default_privileges.keys().next().unwrap();
431 assert_eq!(dp_key.owner, "app_owner");
432 assert_eq!(dp_key.schema, "inventory");
433 assert_eq!(dp_key.on_type, ObjectType::Table);
434 assert_eq!(dp_key.grantee, "inventory-editor");
435 let dp_privs = &graph.default_privileges.values().next().unwrap().privileges;
436 assert!(dp_privs.contains(&Privilege::Select));
437 assert!(dp_privs.contains(&Privilege::Insert));
438
439 assert_eq!(graph.memberships.len(), 1);
441 let edge = graph.memberships.iter().next().unwrap();
442 assert_eq!(edge.role, "inventory-editor");
443 assert_eq!(edge.member, "user@example.com");
444 assert!(edge.inherit);
445 assert!(!edge.admin);
446 }
447
448 #[test]
449 fn grant_privileges_merge_for_same_target() {
450 let yaml = r#"
451roles:
452 - name: testrole
453
454grants:
455 - role: testrole
456 privileges: [SELECT]
457 on: { type: table, schema: public, name: "*" }
458 - role: testrole
459 privileges: [INSERT, UPDATE]
460 on: { type: table, schema: public, name: "*" }
461"#;
462 let manifest = parse_manifest(yaml).unwrap();
463 let expanded = expand_manifest(&manifest).unwrap();
464 let graph = RoleGraph::from_expanded(&expanded, None).unwrap();
465
466 assert_eq!(graph.grants.len(), 1);
468 let grant_state = graph.grants.values().next().unwrap();
469 assert_eq!(grant_state.privileges.len(), 3);
470 assert!(grant_state.privileges.contains(&Privilege::Select));
471 assert!(grant_state.privileges.contains(&Privilege::Insert));
472 assert!(grant_state.privileges.contains(&Privilege::Update));
473 }
474}