1use crate::cli::CliError;
13use crate::cli::executor::DeploymentExecutor;
14use crate::client::{Client, ObjectGrant};
15use crate::info;
16use crate::project::ir::object_id::ObjectId;
17use mz_sql_parser::ast::{
18 GrantPrivilegesStatement, GrantTargetSpecification, GrantTargetSpecificationInner, Ident,
19 ObjectType, Privilege, PrivilegeSpecification, Raw, RevokePrivilegesStatement,
20 UnresolvedItemName, UnresolvedObjectName,
21};
22use owo_colors::{OwoColorize, Stream, Style};
23use std::collections::BTreeSet;
24use std::fmt;
25
26#[derive(Clone, Copy)]
31pub enum GrantObjectKind {
32 Table,
33 Source,
34 Secret,
35 Connection,
36}
37
38impl GrantObjectKind {
39 pub fn catalog_table(&self) -> &'static str {
40 match self {
41 Self::Table => "mz_tables",
42 Self::Source => "mz_sources",
43 Self::Secret => "mz_secrets",
44 Self::Connection => "mz_connections",
45 }
46 }
47
48 pub fn grant_target(&self, obj_id: &ObjectId) -> GrantTargetSpecification<Raw> {
49 let object_type = match self {
50 Self::Table | Self::Source => ObjectType::Table,
51 Self::Secret => ObjectType::Secret,
52 Self::Connection => ObjectType::Connection,
53 };
54 let item_name = UnresolvedItemName::qualified(&[
55 Ident::new_unchecked(obj_id.expect_database()),
56 Ident::new_unchecked(obj_id.schema()),
57 Ident::new_unchecked(obj_id.object()),
58 ]);
59 build_grant_target(object_type, UnresolvedObjectName::Item(item_name))
60 }
61
62 pub fn all_privileges(&self) -> &'static [&'static str] {
63 match self {
64 Self::Table => &["SELECT", "INSERT", "UPDATE", "DELETE"],
65 Self::Source => &["SELECT"],
66 Self::Secret | Self::Connection => &["USAGE"],
67 }
68 }
69
70 pub fn label(&self) -> &'static str {
71 match self {
72 Self::Table => "table",
73 Self::Source => "source",
74 Self::Secret => "secret",
75 Self::Connection => "connection",
76 }
77 }
78
79 pub fn object_type_str(&self) -> &'static str {
81 match self {
82 Self::Table | Self::Source => "table",
83 Self::Secret => "secret",
84 Self::Connection => "connection",
85 }
86 }
87}
88
89fn build_grant_target(
91 object_type: ObjectType,
92 name: UnresolvedObjectName,
93) -> GrantTargetSpecification<Raw> {
94 GrantTargetSpecification::Object {
95 object_type,
96 object_spec_inner: GrantTargetSpecificationInner::Objects { names: vec![name] },
97 }
98}
99
100pub enum GrantNamedObjectKind {
105 Cluster,
106 NetworkPolicy,
107}
108
109impl GrantNamedObjectKind {
110 fn grant_target(&self, name: &str) -> GrantTargetSpecification<Raw> {
111 let (object_type, object_name) = match self {
112 Self::Cluster => (
113 ObjectType::Cluster,
114 UnresolvedObjectName::Cluster(Ident::new_unchecked(name)),
115 ),
116 Self::NetworkPolicy => (
117 ObjectType::NetworkPolicy,
118 UnresolvedObjectName::NetworkPolicy(Ident::new_unchecked(name)),
119 ),
120 };
121 build_grant_target(object_type, object_name)
122 }
123
124 fn all_privileges(&self) -> &'static [&'static str] {
125 match self {
126 Self::Cluster => &["USAGE", "CREATE"],
127 Self::NetworkPolicy => &["USAGE"],
128 }
129 }
130
131 fn label(&self) -> &'static str {
132 match self {
133 Self::Cluster => "cluster",
134 Self::NetworkPolicy => "network policy",
135 }
136 }
137}
138
139pub async fn reconcile_named_object(
146 client: &Client,
147 executor: &DeploymentExecutor<'_>,
148 name: &str,
149 grants: &[GrantPrivilegesStatement<Raw>],
150 kind: &GrantNamedObjectKind,
151) -> Result<(), CliError> {
152 for grant in grants {
153 executor.execute_sql(grant).await?;
154 }
155 let introspection = client.introspection();
156 let (current, default_privs) = match kind {
157 GrantNamedObjectKind::Cluster => (
158 introspection
159 .get_cluster_grants(name)
160 .await
161 .map_err(CliError::Connection)?,
162 introspection
163 .get_default_privilege_grants_for_cluster(name)
164 .await
165 .map_err(CliError::Connection)?,
166 ),
167 GrantNamedObjectKind::NetworkPolicy => (
168 introspection
169 .get_network_policy_grants(name)
170 .await
171 .map_err(CliError::Connection)?,
172 introspection
173 .get_default_privilege_grants_for_network_policy(name)
174 .await
175 .map_err(CliError::Connection)?,
176 ),
177 };
178 let protected: BTreeSet<_> = default_privs
179 .iter()
180 .map(|g| (g.grantee.to_lowercase(), g.privilege_type.to_uppercase()))
181 .collect();
182 let desired = desired_grants(grants, kind.all_privileges());
183 let target = kind.grant_target(name);
184 let revocations = stale_grant_revocations(¤t, &desired, &protected, &target);
185 execute_revocations(executor, &revocations, kind.label(), &name).await
186}
187
188pub async fn reconcile(
195 client: &Client,
196 executor: &DeploymentExecutor<'_>,
197 obj_id: &ObjectId,
198 grants: &[GrantPrivilegesStatement<Raw>],
199 kind: &GrantObjectKind,
200) -> Result<(), CliError> {
201 for grant in grants {
202 executor.execute_sql(grant).await?;
203 }
204 let current = client
205 .introspection()
206 .get_database_object_grants(
207 kind.catalog_table(),
208 obj_id.expect_database(),
209 obj_id.schema(),
210 obj_id.object(),
211 )
212 .await
213 .map_err(CliError::Connection)?;
214 let default_privs = client
215 .introspection()
216 .get_default_privilege_grants_for_database_object(
217 kind.catalog_table(),
218 obj_id.expect_database(),
219 obj_id.schema(),
220 obj_id.object(),
221 kind.object_type_str(),
222 )
223 .await
224 .map_err(CliError::Connection)?;
225 let protected: BTreeSet<_> = default_privs
226 .iter()
227 .map(|g| (g.grantee.to_lowercase(), g.privilege_type.to_uppercase()))
228 .collect();
229 let desired = desired_grants(grants, kind.all_privileges());
230 let target = kind.grant_target(obj_id);
231 let revocations = stale_grant_revocations(¤t, &desired, &protected, &target);
232 execute_revocations(executor, &revocations, kind.label(), obj_id).await
233}
234
235pub fn desired_grants(
240 grants: &[GrantPrivilegesStatement<Raw>],
241 all_privileges: &[&str],
242) -> BTreeSet<(String, String)> {
243 let mut result = BTreeSet::new();
244 for grant in grants {
245 let privs: Vec<String> = match &grant.privileges {
246 PrivilegeSpecification::All => all_privileges.iter().map(|p| p.to_string()).collect(),
247 PrivilegeSpecification::Privileges(privs) => {
248 privs.iter().map(|p| p.to_string()).collect()
249 }
250 };
251 for role in &grant.roles {
252 let role_name = role.as_str().to_lowercase();
253 for priv_name in &privs {
254 result.insert((role_name.clone(), priv_name.clone()));
255 }
256 }
257 }
258 result
259}
260
261fn parse_privilege(s: &str) -> Option<Privilege> {
268 let p = if s.eq_ignore_ascii_case("SELECT") {
269 Privilege::SELECT
270 } else if s.eq_ignore_ascii_case("INSERT") {
271 Privilege::INSERT
272 } else if s.eq_ignore_ascii_case("UPDATE") {
273 Privilege::UPDATE
274 } else if s.eq_ignore_ascii_case("DELETE") {
275 Privilege::DELETE
276 } else if s.eq_ignore_ascii_case("USAGE") {
277 Privilege::USAGE
278 } else if s.eq_ignore_ascii_case("CREATE") {
279 Privilege::CREATE
280 } else if s.eq_ignore_ascii_case("CREATEROLE") {
281 Privilege::CREATEROLE
282 } else if s.eq_ignore_ascii_case("CREATEDB") {
283 Privilege::CREATEDB
284 } else if s.eq_ignore_ascii_case("CREATECLUSTER") {
285 Privilege::CREATECLUSTER
286 } else if s.eq_ignore_ascii_case("CREATENETWORKPOLICY") {
287 Privilege::CREATENETWORKPOLICY
288 } else {
289 return None;
290 };
291 Some(p)
292}
293
294pub fn stale_grant_revocations(
303 current: &[ObjectGrant],
304 desired: &BTreeSet<(String, String)>,
305 protected: &BTreeSet<(String, String)>,
306 target: &GrantTargetSpecification<Raw>,
307) -> Vec<RevokePrivilegesStatement<Raw>> {
308 let mut revocations = Vec::new();
309 for grant in current {
310 let key = (
311 grant.grantee.to_lowercase(),
312 grant.privilege_type.to_uppercase(),
313 );
314 if desired.contains(&key) || protected.contains(&key) {
315 continue;
316 }
317 let Some(privilege) = parse_privilege(&grant.privilege_type) else {
318 crate::verbose!(
319 "skipping revocation of unknown privilege '{}' on grantee '{}' (target: {:?})",
320 grant.privilege_type,
321 grant.grantee,
322 target,
323 );
324 continue;
325 };
326 revocations.push(RevokePrivilegesStatement {
327 privileges: PrivilegeSpecification::Privileges(vec![privilege]),
328 target: target.clone(),
329 roles: vec![Ident::new_unchecked(grant.grantee.clone())],
330 });
331 }
332 revocations
333}
334
335pub async fn execute_revocations(
337 executor: &DeploymentExecutor<'_>,
338 revocations: &[RevokePrivilegesStatement<Raw>],
339 object_type_label: &str,
340 display_name: &impl fmt::Display,
341) -> Result<(), CliError> {
342 let dash_style = Style::new().red().bold();
343 for stmt in revocations {
344 if !executor.is_dry_run() {
345 info!(
346 " {} Revoking stale grant on {} '{}'",
347 "-".if_supports_color(Stream::Stderr, |t| dash_style.style(t)),
348 object_type_label,
349 display_name,
350 );
351 }
352 executor.execute_sql(stmt).await?;
353 }
354 Ok(())
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360 use mz_sql_parser::ast::Statement;
361 use mz_sql_parser::parser::parse_statements;
362
363 fn make_object_grant(grantee: &str, privilege_type: &str) -> ObjectGrant {
364 ObjectGrant {
365 grantee: grantee.to_string(),
366 privilege_type: privilege_type.to_string(),
367 }
368 }
369
370 fn parse_grant(sql: &str) -> GrantPrivilegesStatement<Raw> {
372 let stmts = parse_statements(sql).unwrap();
373 match stmts.into_iter().next().unwrap().ast {
374 Statement::GrantPrivileges(g) => g,
375 other => panic!("expected GRANT, got: {}", other),
376 }
377 }
378
379 fn cluster_target(name: &str) -> GrantTargetSpecification<Raw> {
380 GrantNamedObjectKind::Cluster.grant_target(name)
381 }
382
383 fn network_policy_target(name: &str) -> GrantTargetSpecification<Raw> {
384 GrantNamedObjectKind::NetworkPolicy.grant_target(name)
385 }
386
387 fn obj_id(db: &str, schema: &str, name: &str) -> ObjectId {
388 ObjectId::new(db.to_string(), schema.to_string(), name.to_string())
389 }
390
391 fn table_target(db: &str, schema: &str, name: &str) -> GrantTargetSpecification<Raw> {
392 GrantObjectKind::Table.grant_target(&obj_id(db, schema, name))
393 }
394
395 fn secret_target(db: &str, schema: &str, name: &str) -> GrantTargetSpecification<Raw> {
396 GrantObjectKind::Secret.grant_target(&obj_id(db, schema, name))
397 }
398
399 fn connection_target(db: &str, schema: &str, name: &str) -> GrantTargetSpecification<Raw> {
400 GrantObjectKind::Connection.grant_target(&obj_id(db, schema, name))
401 }
402
403 fn source_target(db: &str, schema: &str, name: &str) -> GrantTargetSpecification<Raw> {
404 GrantObjectKind::Source.grant_target(&obj_id(db, schema, name))
405 }
406
407 fn to_strings(revocations: &[RevokePrivilegesStatement<Raw>]) -> Vec<String> {
409 revocations.iter().map(|r| r.to_string()).collect()
410 }
411
412 #[mz_ore::test]
413 fn test_desired_grants_single_privilege_single_role() {
414 let grant = parse_grant("GRANT USAGE ON CLUSTER my_cluster TO reader");
415 let result = desired_grants(&[grant], &["USAGE", "CREATE"]);
416 assert_eq!(result.len(), 1);
417 assert!(result.contains(&("reader".to_string(), "USAGE".to_string())));
418 }
419
420 #[mz_ore::test]
421 fn test_desired_grants_multiple_privileges() {
422 let grant = parse_grant("GRANT USAGE, CREATE ON CLUSTER my_cluster TO writer");
423 let result = desired_grants(&[grant], &["USAGE", "CREATE"]);
424 assert_eq!(result.len(), 2);
425 assert!(result.contains(&("writer".to_string(), "USAGE".to_string())));
426 assert!(result.contains(&("writer".to_string(), "CREATE".to_string())));
427 }
428
429 #[mz_ore::test]
430 fn test_desired_grants_all_expands_to_object_type_privileges() {
431 let grant = parse_grant("GRANT ALL ON CLUSTER my_cluster TO admin");
432 let result = desired_grants(&[grant], &["USAGE", "CREATE"]);
434 assert_eq!(result.len(), 2);
435 assert!(result.contains(&("admin".to_string(), "USAGE".to_string())));
436 assert!(result.contains(&("admin".to_string(), "CREATE".to_string())));
437 }
438
439 #[mz_ore::test]
440 fn test_desired_grants_all_with_single_privilege_object_type() {
441 let grant = parse_grant("GRANT ALL ON SECRET \"db\".\"public\".\"my_secret\" TO reader");
442 let result = desired_grants(&[grant], &["USAGE"]);
444 assert_eq!(result.len(), 1);
445 assert!(result.contains(&("reader".to_string(), "USAGE".to_string())));
446 }
447
448 #[mz_ore::test]
449 fn test_desired_grants_multiple_roles() {
450 let grant = parse_grant("GRANT USAGE ON CLUSTER my_cluster TO reader, writer");
451 let result = desired_grants(&[grant], &["USAGE", "CREATE"]);
452 assert_eq!(result.len(), 2);
453 assert!(result.contains(&("reader".to_string(), "USAGE".to_string())));
454 assert!(result.contains(&("writer".to_string(), "USAGE".to_string())));
455 }
456
457 #[mz_ore::test]
458 fn test_desired_grants_multiple_grant_statements() {
459 let g1 = parse_grant("GRANT USAGE ON CLUSTER my_cluster TO reader");
460 let g2 = parse_grant("GRANT CREATE ON CLUSTER my_cluster TO writer");
461 let result = desired_grants(&[g1, g2], &["USAGE", "CREATE"]);
462 assert_eq!(result.len(), 2);
463 assert!(result.contains(&("reader".to_string(), "USAGE".to_string())));
464 assert!(result.contains(&("writer".to_string(), "CREATE".to_string())));
465 }
466
467 #[mz_ore::test]
468 fn test_desired_grants_deduplicates() {
469 let g1 = parse_grant("GRANT USAGE ON CLUSTER my_cluster TO reader");
471 let g2 = parse_grant("GRANT USAGE ON CLUSTER my_cluster TO reader");
472 let result = desired_grants(&[g1, g2], &["USAGE", "CREATE"]);
473 assert_eq!(result.len(), 1);
474 }
475
476 #[mz_ore::test]
477 fn test_desired_grants_empty_input() {
478 let result = desired_grants(&[], &["USAGE", "CREATE"]);
479 assert!(result.is_empty());
480 }
481
482 #[mz_ore::test]
483 fn test_desired_grants_role_name_lowercased() {
484 let grant = parse_grant("GRANT USAGE ON CLUSTER my_cluster TO \"MyRole\"");
485 let result = desired_grants(&[grant], &["USAGE"]);
486 assert!(result.contains(&("myrole".to_string(), "USAGE".to_string())));
487 }
488
489 #[mz_ore::test]
490 fn test_desired_grants_table_all_privileges() {
491 let grant = parse_grant("GRANT ALL ON TABLE \"db\".\"public\".\"my_table\" TO admin");
492 let result = desired_grants(&[grant], &["SELECT", "INSERT", "UPDATE", "DELETE"]);
494 assert_eq!(result.len(), 4);
495 assert!(result.contains(&("admin".to_string(), "SELECT".to_string())));
496 assert!(result.contains(&("admin".to_string(), "INSERT".to_string())));
497 assert!(result.contains(&("admin".to_string(), "UPDATE".to_string())));
498 assert!(result.contains(&("admin".to_string(), "DELETE".to_string())));
499 }
500
501 #[mz_ore::test]
502 fn test_stale_grant_revocations_no_stale() {
503 let current = vec![make_object_grant("reader", "USAGE")];
504 let mut desired = BTreeSet::new();
505 desired.insert(("reader".to_string(), "USAGE".to_string()));
506
507 let target = cluster_target("my_cluster");
508 let revocations = stale_grant_revocations(¤t, &desired, &BTreeSet::new(), &target);
509 assert!(revocations.is_empty());
510 }
511
512 #[mz_ore::test]
513 fn test_stale_grant_revocations_has_stale() {
514 let current = vec![
515 make_object_grant("reader", "USAGE"),
516 make_object_grant("writer", "CREATE"),
517 ];
518 let mut desired = BTreeSet::new();
519 desired.insert(("reader".to_string(), "USAGE".to_string()));
520
521 let target = cluster_target("my_cluster");
522 let revocations = stale_grant_revocations(¤t, &desired, &BTreeSet::new(), &target);
523 let strings = to_strings(&revocations);
524 assert_eq!(strings.len(), 1);
525 assert_eq!(
526 strings[0],
527 "REVOKE CREATE ON CLUSTER my_cluster FROM writer"
528 );
529 }
530
531 #[mz_ore::test]
532 fn test_stale_grant_revocations_empty_desired() {
533 let current = vec![make_object_grant("reader", "USAGE")];
534 let desired = BTreeSet::new();
535
536 let target = table_target("db", "public", "t");
537 let revocations = stale_grant_revocations(¤t, &desired, &BTreeSet::new(), &target);
538 let strings = to_strings(&revocations);
539 assert_eq!(strings.len(), 1);
540 assert_eq!(strings[0], "REVOKE USAGE ON TABLE db.public.t FROM reader");
541 }
542
543 #[mz_ore::test]
544 fn test_stale_grant_revocations_empty_current() {
545 let mut desired = BTreeSet::new();
546 desired.insert(("reader".to_string(), "USAGE".to_string()));
547
548 let target = cluster_target("my_cluster");
549 let revocations = stale_grant_revocations(&[], &desired, &BTreeSet::new(), &target);
550 assert!(revocations.is_empty());
551 }
552
553 #[mz_ore::test]
554 fn test_stale_grant_revocations_both_empty() {
555 let target = secret_target("db", "public", "s");
556 let revocations = stale_grant_revocations(&[], &BTreeSet::new(), &BTreeSet::new(), &target);
557 assert!(revocations.is_empty());
558 }
559
560 #[mz_ore::test]
561 fn test_stale_grant_revocations_case_insensitive_match() {
562 let current = vec![make_object_grant("Reader", "usage")];
564 let mut desired = BTreeSet::new();
565 desired.insert(("reader".to_string(), "USAGE".to_string()));
566
567 let target = cluster_target("my_cluster");
568 let revocations = stale_grant_revocations(¤t, &desired, &BTreeSet::new(), &target);
569 assert!(revocations.is_empty());
570 }
571
572 #[mz_ore::test]
573 fn test_stale_grant_revocations_multiple_stale() {
574 let current = vec![
575 make_object_grant("reader", "USAGE"),
576 make_object_grant("writer", "CREATE"),
577 make_object_grant("admin", "USAGE"),
578 ];
579 let desired = BTreeSet::new(); let target = cluster_target("my_cluster");
582 let revocations = stale_grant_revocations(¤t, &desired, &BTreeSet::new(), &target);
583 assert_eq!(revocations.len(), 3);
584 }
585
586 #[mz_ore::test]
587 fn test_stale_grant_revocations_network_policy_keyword() {
588 let current = vec![make_object_grant("reader", "USAGE")];
589 let desired = BTreeSet::new();
590
591 let target = network_policy_target("my_policy");
592 let revocations = stale_grant_revocations(¤t, &desired, &BTreeSet::new(), &target);
593 let strings = to_strings(&revocations);
594 assert_eq!(strings.len(), 1);
595 assert_eq!(
596 strings[0],
597 "REVOKE USAGE ON NETWORK POLICY my_policy FROM reader"
598 );
599 }
600
601 #[mz_ore::test]
602 fn test_stale_grant_revocations_connection_keyword() {
603 let current = vec![make_object_grant("app", "USAGE")];
604 let desired = BTreeSet::new();
605
606 let target = connection_target("db", "public", "my_conn");
607 let revocations = stale_grant_revocations(¤t, &desired, &BTreeSet::new(), &target);
608 let strings = to_strings(&revocations);
609 assert_eq!(strings.len(), 1);
610 assert_eq!(
611 strings[0],
612 "REVOKE USAGE ON CONNECTION db.public.my_conn FROM app"
613 );
614 }
615
616 #[mz_ore::test]
617 fn test_stale_grant_revocations_secret_keyword() {
618 let current = vec![make_object_grant("app", "USAGE")];
619 let desired = BTreeSet::new();
620
621 let target = secret_target("db", "public", "my_secret");
622 let revocations = stale_grant_revocations(¤t, &desired, &BTreeSet::new(), &target);
623 let strings = to_strings(&revocations);
624 assert_eq!(strings.len(), 1);
625 assert_eq!(
626 strings[0],
627 "REVOKE USAGE ON SECRET db.public.my_secret FROM app"
628 );
629 }
630
631 #[mz_ore::test]
632 fn test_stale_grant_revocations_source_keyword() {
633 let current = vec![make_object_grant("reader", "SELECT")];
634 let desired = BTreeSet::new();
635
636 let target = source_target("db", "public", "my_source");
637 let revocations = stale_grant_revocations(¤t, &desired, &BTreeSet::new(), &target);
638 let strings = to_strings(&revocations);
639 assert_eq!(strings.len(), 1);
640 assert_eq!(
641 strings[0],
642 "REVOKE SELECT ON TABLE db.public.my_source FROM reader"
643 );
644 }
645
646 #[mz_ore::test]
647 fn test_stale_grant_revocations_protected_grants_not_revoked() {
648 let current = vec![
651 make_object_grant("reader", "SELECT"),
652 make_object_grant("writer", "SELECT"),
653 ];
654 let desired = BTreeSet::new();
655 let mut protected = BTreeSet::new();
656 protected.insert(("reader".to_string(), "SELECT".to_string()));
657
658 let target = table_target("db", "public", "t");
659 let revocations = stale_grant_revocations(¤t, &desired, &protected, &target);
660 let strings = to_strings(&revocations);
661 assert_eq!(strings.len(), 1);
662 assert!(strings[0].contains("writer"));
663 assert!(!strings[0].contains("reader"));
664 }
665
666 #[mz_ore::test]
667 fn test_end_to_end_no_revocations_when_grants_match() {
668 let grant = parse_grant("GRANT USAGE ON CLUSTER my_cluster TO reader");
669 let desired = desired_grants(&[grant], &["USAGE", "CREATE"]);
670 let current = vec![make_object_grant("reader", "USAGE")];
671
672 let target = cluster_target("my_cluster");
673 let revocations = stale_grant_revocations(¤t, &desired, &BTreeSet::new(), &target);
674 assert!(revocations.is_empty());
675 }
676
677 #[mz_ore::test]
678 fn test_end_to_end_revoke_removed_grant() {
679 let grant = parse_grant("GRANT USAGE ON CLUSTER my_cluster TO reader");
681 let desired = desired_grants(&[grant], &["USAGE", "CREATE"]);
682 let current = vec![
683 make_object_grant("reader", "USAGE"),
684 make_object_grant("writer", "CREATE"),
685 ];
686
687 let target = cluster_target("my_cluster");
688 let revocations = stale_grant_revocations(¤t, &desired, &BTreeSet::new(), &target);
689 let strings = to_strings(&revocations);
690 assert_eq!(strings.len(), 1);
691 assert!(strings[0].contains("writer"));
692 assert!(strings[0].contains("CREATE"));
693 }
694
695 #[mz_ore::test]
696 fn test_end_to_end_revoke_all_when_grants_removed() {
697 let desired = desired_grants(&[], &["USAGE", "CREATE"]);
699 let current = vec![
700 make_object_grant("reader", "USAGE"),
701 make_object_grant("writer", "CREATE"),
702 ];
703
704 let target = cluster_target("my_cluster");
705 let revocations = stale_grant_revocations(¤t, &desired, &BTreeSet::new(), &target);
706 assert_eq!(revocations.len(), 2);
707 }
708
709 #[mz_ore::test]
710 fn test_end_to_end_grant_all_covers_all_current() {
711 let grant = parse_grant("GRANT ALL ON CLUSTER my_cluster TO admin");
712 let desired = desired_grants(&[grant], &["USAGE", "CREATE"]);
713 let current = vec![
715 make_object_grant("admin", "USAGE"),
716 make_object_grant("admin", "CREATE"),
717 ];
718
719 let target = cluster_target("my_cluster");
720 let revocations = stale_grant_revocations(¤t, &desired, &BTreeSet::new(), &target);
721 assert!(revocations.is_empty());
722 }
723
724 #[mz_ore::test]
725 fn test_end_to_end_grant_all_still_revokes_other_roles() {
726 let grant = parse_grant("GRANT ALL ON CLUSTER my_cluster TO admin");
727 let desired = desired_grants(&[grant], &["USAGE", "CREATE"]);
728 let current = vec![
730 make_object_grant("admin", "USAGE"),
731 make_object_grant("admin", "CREATE"),
732 make_object_grant("reader", "USAGE"),
733 ];
734
735 let target = cluster_target("my_cluster");
736 let revocations = stale_grant_revocations(¤t, &desired, &BTreeSet::new(), &target);
737 let strings = to_strings(&revocations);
738 assert_eq!(strings.len(), 1);
739 assert!(strings[0].contains("reader"));
740 }
741
742 #[mz_ore::test]
743 fn test_end_to_end_multiple_roles_multiple_privileges() {
744 let g1 = parse_grant("GRANT USAGE ON CLUSTER c TO reader");
745 let g2 = parse_grant("GRANT USAGE, CREATE ON CLUSTER c TO writer");
746 let desired = desired_grants(&[g1, g2], &["USAGE", "CREATE"]);
747
748 let current = vec![
750 make_object_grant("reader", "USAGE"),
751 make_object_grant("writer", "USAGE"),
752 make_object_grant("writer", "CREATE"),
753 make_object_grant("admin", "USAGE"),
754 ];
755
756 let target = cluster_target("c");
757 let revocations = stale_grant_revocations(¤t, &desired, &BTreeSet::new(), &target);
758 let strings = to_strings(&revocations);
759 assert_eq!(strings.len(), 1);
760 assert!(strings[0].contains("admin"));
761 }
762}