mz_mysql_util/
privileges.rs1use std::collections::{BTreeMap, BTreeSet};
11use std::sync::LazyLock;
12
13use fancy_regex::Regex;
14use mysql_async::prelude::Queryable;
15
16use crate::tunnel::MySqlConn;
17use crate::{MissingPrivilege, MySqlError, QualifiedTableRef};
18
19pub async fn validate_source_privileges(
20 conn: &mut MySqlConn,
21 tables: &[QualifiedTableRef<'_>],
22) -> Result<(), MySqlError> {
23 let roles: Option<String> = conn
32 .exec_first("SELECT CURRENT_ROLE()", ())
33 .await
34 .ok()
35 .and_then(|val: Option<String>| match val {
36 Some(inner) if inner != "NONE" => Some(inner),
37 _ => None,
38 });
39
40 let grant_query = match roles {
42 None => "SHOW GRANTS FOR CURRENT_USER()".to_string(),
43 Some(roles) => format!("SHOW GRANTS FOR CURRENT_USER() USING {}", roles),
44 };
45
46 let mut grant_map = BTreeMap::new();
48 for grant in conn.exec(grant_query, ()).await? {
49 let grant: String = grant;
50 if let Some(object_grant) = get_object_grant(&grant) {
51 grant_map
52 .entry(object_grant.object_schema)
53 .or_insert_with(BTreeMap::new)
54 .entry(object_grant.object_name)
55 .or_insert_with(BTreeSet::new)
56 .extend(object_grant.privileges);
57 }
58 }
59
60 let mut errors = tables
62 .iter()
63 .flat_map(|table| {
64 ["LOCK TABLES", "SELECT"].iter().filter_map(|privilege| {
65 let privileged = [grant_map.get("*"), grant_map.get(table.schema_name)]
67 .iter()
68 .any(|schema_map| {
69 schema_map.map_or(false, |schema_map| {
71 [schema_map.get("*"), schema_map.get(table.table_name)]
72 .iter()
73 .any(|privs| {
74 privs.map_or(false, |privs| {
75 privs.contains(*privilege)
76 || privs.contains("ALL PRIVILEGES")
77 })
78 })
79 })
80 });
81
82 if !privileged {
83 Some(MissingPrivilege {
84 privilege: privilege.to_string(),
85 qualified_table_name: format!("{}.{}", table.schema_name, table.table_name),
86 })
87 } else {
88 None
89 }
90 })
91 })
92 .collect::<Vec<_>>();
93
94 if !grant_map
96 .get("*")
97 .and_then(|schema_map| schema_map.get("*"))
98 .map(|privs| privs.contains("REPLICATION SLAVE") || privs.contains("ALL PRIVILEGES"))
99 .unwrap_or(false)
100 {
101 errors.push(MissingPrivilege {
102 privilege: "REPLICATION SLAVE".to_string(),
103 qualified_table_name: "*.*".to_string(),
104 });
105 }
106
107 if !errors.is_empty() {
108 Err(MySqlError::MissingPrivileges(errors))
109 } else {
110 Ok(())
111 }
112}
113
114static GRANT_REGEX: LazyLock<Regex> = LazyLock::new(|| {
130 Regex::new(r#"GRANT (.+) ON (\*|(['`"])(.*)\3).(\*|(['`"])(.*)\6) TO (['`"])(.*)\8@.*"#)
131 .expect("valid")
132});
133
134#[derive(Debug, PartialEq, Eq)]
135struct MySqlObjectGrant {
136 privileges: BTreeSet<String>,
137 object_schema: String,
138 object_name: String,
139}
140
141fn get_object_grant(grant: &str) -> Option<MySqlObjectGrant> {
144 match GRANT_REGEX.captures(grant) {
145 Ok(None) => None,
146 Err(err) => {
147 tracing::warn!("Error parsing privilege grant: {}", err);
148 None
149 }
150 Ok(Some(captures)) => {
151 let object_schema = if captures.get(2).expect("valid").as_str() == "*" {
152 "*".to_string()
153 } else {
154 captures.get(4).expect("valid").as_str().to_string()
155 };
156 let object_name = if captures.get(5).expect("valid").as_str() == "*" {
157 "*".to_string()
158 } else {
159 captures.get(7).expect("valid").as_str().to_string()
160 };
161 let privileges = captures.get(1).expect("valid").as_str();
162 Some(MySqlObjectGrant {
163 privileges: privileges.split(", ").map(|s| s.to_string()).collect(),
164 object_schema,
165 object_name,
166 })
167 }
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[mz_ore::test]
176 fn test_get_object_grant() {
177 let grant = "GRANT SELECT, INSERT, UPDATE ON `db1`.* TO `u1`@`localhost`";
179 let expected_grant = MySqlObjectGrant {
180 privileges: ["SELECT", "INSERT", "UPDATE"]
181 .iter()
182 .map(|s| s.to_string())
183 .collect(),
184 object_schema: "db1".to_string(),
185 object_name: "*".to_string(),
186 };
187 assert_eq!(get_object_grant(grant), Some(expected_grant));
188
189 let grant = "GRANT SUPER, CREATE TEMPORARY TABLES, LOCK TABLES ON 'db1'.'table1' TO `u1`@`localhost`";
191 let expected_grant = MySqlObjectGrant {
192 privileges: ["SUPER", "CREATE TEMPORARY TABLES", "LOCK TABLES"]
193 .iter()
194 .map(|s| s.to_string())
195 .collect(),
196 object_schema: "db1".to_string(),
197 object_name: "table1".to_string(),
198 };
199 assert_eq!(get_object_grant(grant), Some(expected_grant));
200
201 let grant = "GRANT ALL PRIVILEGES ON *.* TO `u1`@`localhost`";
203 let expected_grant = MySqlObjectGrant {
204 privileges: ["ALL PRIVILEGES"].iter().map(|s| s.to_string()).collect(),
205 object_schema: "*".to_string(),
206 object_name: "*".to_string(),
207 };
208 assert_eq!(get_object_grant(grant), Some(expected_grant));
209
210 let grant = "GRANT SELECT, INSERT, UPDATE ON `таблица`.`mixED_CAse` TO `u1`@`localhost`";
212 let expected_grant = MySqlObjectGrant {
213 privileges: ["SELECT", "INSERT", "UPDATE"]
214 .iter()
215 .map(|s| s.to_string())
216 .collect(),
217 object_schema: "таблица".to_string(),
218 object_name: "mixED_CAse".to_string(),
219 };
220
221 assert_eq!(get_object_grant(grant), Some(expected_grant));
222
223 let grant = "GRANT SUPER, CREATE TEMPORARY TABLES, LOCK TABLES ON `r`w`.`'sd'` TO `u1`@`localhost` IDENTIFIED BY PASSWORD `test`";
225 let expected_grant = MySqlObjectGrant {
226 privileges: ["SUPER", "CREATE TEMPORARY TABLES", "LOCK TABLES"]
227 .iter()
228 .map(|s| s.to_string())
229 .collect(),
230 object_schema: "r`w".to_string(),
231 object_name: "'sd'".to_string(),
232 };
233 assert_eq!(get_object_grant(grant), Some(expected_grant));
234 }
235}