1use crate::catalog::roles::identity::RoleSubject;
10use std::collections::{BTreeMap, BTreeSet};
11use uqa_core::catalog_acl::AclGrantee;
12
13use crate::ast::{DatabasePrivilege, DatabaseRevokeBehavior, GrantDatabaseStmt, RoleAttribute};
14use crate::catalog::DATABASE_NAME;
15use crate::SQLError;
16
17use crate::catalog::roles::{role_inherits, RoleDefinition, RoleMembership, RoleMembershipKey};
18
19pub mod binding;
20pub use binding::BoundDatabaseSecurity;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
23pub enum DatabaseAclPrivilege {
24 Connect,
25 Create,
26 Temporary,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct DatabasePrivilegeCheck {
31 pub privilege: DatabaseAclPrivilege,
32 pub grant_option: bool,
33}
34
35impl DatabaseAclPrivilege {
36 const fn mask(self) -> DatabasePrivileges {
37 match self {
38 Self::Connect => DatabasePrivileges {
39 connect: true,
40 create: false,
41 temporary: false,
42 },
43 Self::Create => DatabasePrivileges {
44 connect: false,
45 create: true,
46 temporary: false,
47 },
48 Self::Temporary => DatabasePrivileges {
49 connect: false,
50 create: false,
51 temporary: true,
52 },
53 }
54 }
55}
56
57pub fn requested_acl_privileges(
58 requested: &[DatabasePrivilege],
59) -> Result<Vec<DatabaseAclPrivilege>, SQLError> {
60 requested
61 .iter()
62 .map(|privilege| match privilege {
63 DatabasePrivilege::Connect => Ok(DatabaseAclPrivilege::Connect),
64 DatabasePrivilege::Create => Ok(DatabaseAclPrivilege::Create),
65 DatabasePrivilege::Temporary => Ok(DatabaseAclPrivilege::Temporary),
66 DatabasePrivilege::Unsupported(name) => Err(SQLError::Routine {
67 sqlstate: "0LP01".into(),
68 message: format!("invalid privilege type {name} for database"),
69 }),
70 })
71 .collect()
72}
73
74pub fn parse_privilege_checks(value: &str) -> Result<Vec<DatabasePrivilegeCheck>, SQLError> {
75 value
76 .split(',')
77 .map(|item| {
78 let item = item.trim();
79 let upper = item.to_ascii_uppercase();
80 let (name, grant_option) = upper
81 .strip_suffix(" WITH GRANT OPTION")
82 .map_or((upper.as_str(), false), |name| (name.trim_end(), true));
83 let privilege = match name {
84 "CONNECT" => DatabaseAclPrivilege::Connect,
85 "CREATE" => DatabaseAclPrivilege::Create,
86 "TEMP" | "TEMPORARY" => DatabaseAclPrivilege::Temporary,
87 _ => {
88 return Err(SQLError::Routine {
89 sqlstate: "22023".into(),
90 message: format!("unrecognized privilege type: \"{item}\""),
91 })
92 }
93 };
94 Ok(DatabasePrivilegeCheck {
95 privilege,
96 grant_option,
97 })
98 })
99 .collect()
100}
101
102fn acl_grantor<'a>(entry: &'a DatabaseAclEntry, owner: &'a str) -> &'a str {
103 entry.grantor.as_deref().unwrap_or(owner)
104}
105
106fn materialize_acl(security: &mut DatabaseSecurity) {
107 if security.acl.is_some() {
108 return;
109 }
110 let owner = security.role_owner.clone();
111 security.acl = Some(vec![
112 DatabaseAclEntry {
113 role: owner.clone().into(),
114 grantor: Some(owner.clone()),
115 privileges: DatabasePrivileges::ALL,
116 grant_options: DatabasePrivileges::default(),
117 },
118 DatabaseAclEntry {
119 role: AclGrantee::Public,
120 grantor: Some(owner),
121 privileges: DatabasePrivileges {
122 connect: true,
123 create: false,
124 temporary: true,
125 },
126 grant_options: DatabasePrivileges::default(),
127 },
128 ]);
129}
130
131fn grant_option_roles(
132 security: &DatabaseSecurity,
133 privilege: DatabaseAclPrivilege,
134) -> BTreeSet<String> {
135 let mut reachable = BTreeSet::from([security.role_owner.clone()]);
136 let Some(acl) = security.acl.as_ref() else {
137 return reachable;
138 };
139 loop {
140 let mut changed = false;
141 for entry in acl {
142 let Some(role) = entry.role.role_name() else {
143 continue;
144 };
145 if entry.grant_options.intersects(privilege.mask())
146 && reachable.contains(acl_grantor(entry, &security.role_owner))
147 {
148 changed |= reachable.insert(role.to_owned());
149 }
150 }
151 if !changed {
152 return reachable;
153 }
154 }
155}
156
157pub fn select_acl_grantor(
158 security: &DatabaseSecurity,
159 privilege: DatabaseAclPrivilege,
160 current_user: &(impl RoleSubject + ?Sized),
161 roles: &BTreeMap<String, RoleDefinition>,
162 memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
163) -> Option<String> {
164 let current_user = current_user.role_name(roles)?;
165 if role_inherits(roles, memberships, current_user, &security.role_owner) {
166 return Some(security.role_owner.clone());
167 }
168 let grant_options = grant_option_roles(security, privilege);
169 if grant_options.contains(current_user) {
170 return Some(current_user.to_string());
171 }
172 security.acl.as_ref().and_then(|acl| {
173 acl.iter()
174 .filter_map(|entry| entry.role.role_name())
175 .filter(|role| grant_options.contains(*role))
176 .find(|role| role_inherits(roles, memberships, current_user, *role))
177 .map(str::to_owned)
178 })
179}
180
181pub fn role_has_database_privilege_check(
182 security: &DatabaseSecurity,
183 subject: &(impl RoleSubject + ?Sized),
184 check: DatabasePrivilegeCheck,
185 roles: &BTreeMap<String, RoleDefinition>,
186 memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
187) -> bool {
188 if subject
189 .role_definition(roles)
190 .is_some_and(|role| role.has(RoleAttribute::Superuser))
191 {
192 return true;
193 }
194 if check.grant_option {
195 return grant_option_roles(security, check.privilege)
196 .iter()
197 .any(|role| role_inherits(roles, memberships, subject, role));
198 }
199 match security.acl.as_ref() {
200 None => {
201 role_inherits(roles, memberships, subject, &security.role_owner)
202 || matches!(
203 check.privilege,
204 DatabaseAclPrivilege::Connect | DatabaseAclPrivilege::Temporary
205 )
206 }
207 Some(acl) => acl.iter().any(|entry| {
208 entry.privileges.intersects(check.privilege.mask())
209 && (entry.role.is_public()
210 || role_inherits(roles, memberships, subject, &entry.role))
211 }),
212 }
213}
214
215pub fn role_has_database_privilege(
216 security: &DatabaseSecurity,
217 subject: &(impl RoleSubject + ?Sized),
218 privilege: DatabaseAclPrivilege,
219 roles: &BTreeMap<String, RoleDefinition>,
220 memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
221) -> bool {
222 role_has_database_privilege_check(
223 security,
224 subject,
225 DatabasePrivilegeCheck {
226 privilege,
227 grant_option: false,
228 },
229 roles,
230 memberships,
231 )
232}
233
234pub fn grant_acl(
235 security: &mut DatabaseSecurity,
236 privilege: DatabaseAclPrivilege,
237 grantees: &[AclGrantee],
238 grantor: &str,
239 grant_option: bool,
240) {
241 materialize_acl(security);
242 let owner = security.role_owner.clone();
243 let acl = security
244 .acl
245 .as_mut()
246 .expect("database ACL was materialized");
247 for grantee in grantees {
248 let position = acl
249 .iter()
250 .position(|entry| entry.role == *grantee && acl_grantor(entry, &owner) == grantor)
251 .unwrap_or_else(|| {
252 acl.push(DatabaseAclEntry {
253 role: grantee.clone(),
254 grantor: Some(grantor.to_string()),
255 privileges: DatabasePrivileges::default(),
256 grant_options: DatabasePrivileges::default(),
257 });
258 acl.len() - 1
259 });
260 let entry = &mut acl[position];
261 entry.privileges.insert(privilege.mask());
262 if grant_option && grantee.role_name().is_some_and(|name| name != owner) {
263 entry.grant_options.insert(privilege.mask());
264 }
265 }
266}
267
268pub fn revoke_acl(
269 security: &mut DatabaseSecurity,
270 privilege: DatabaseAclPrivilege,
271 grantees: &[AclGrantee],
272 grantor: &str,
273 grant_option_only: bool,
274 cascade: bool,
275) -> Result<(), SQLError> {
276 let before = grant_option_roles(security, privilege);
277 materialize_acl(security);
278 let owner = security.role_owner.clone();
279 let acl = security
280 .acl
281 .as_mut()
282 .expect("database ACL was materialized");
283 for entry in acl
284 .iter_mut()
285 .filter(|entry| grantees.contains(&entry.role) && acl_grantor(entry, &owner) == grantor)
286 {
287 entry.grant_options.remove(privilege.mask());
288 if !grant_option_only {
289 entry.privileges.remove(privilege.mask());
290 }
291 }
292 remove_empty_entries(acl);
293 revoke_dependent_acl(security, privilege, &before, cascade)
294}
295
296fn revoke_dependent_acl(
297 security: &mut DatabaseSecurity,
298 privilege: DatabaseAclPrivilege,
299 before: &BTreeSet<String>,
300 cascade: bool,
301) -> Result<(), SQLError> {
302 loop {
303 let current = grant_option_roles(security, privilege);
304 let lost = before
305 .difference(¤t)
306 .cloned()
307 .collect::<BTreeSet<_>>();
308 if lost.is_empty() {
309 return Ok(());
310 }
311 let owner = security.role_owner.clone();
312 let dependent = security.acl.as_ref().is_some_and(|acl| {
313 acl.iter().any(|entry| {
314 lost.contains(acl_grantor(entry, &owner))
315 && (entry.privileges.intersects(privilege.mask())
316 || entry.grant_options.intersects(privilege.mask()))
317 })
318 });
319 if !dependent {
320 return Ok(());
321 }
322 if !cascade {
323 return Err(SQLError::Routine {
324 sqlstate: "2BP01".into(),
325 message: "dependent privileges exist".into(),
326 });
327 }
328 let acl = security
329 .acl
330 .as_mut()
331 .expect("dependent database privileges require an explicit ACL");
332 for entry in acl
333 .iter_mut()
334 .filter(|entry| lost.contains(acl_grantor(entry, &owner)))
335 {
336 entry.privileges.remove(privilege.mask());
337 entry.grant_options.remove(privilege.mask());
338 }
339 remove_empty_entries(acl);
340 }
341}
342
343fn remove_empty_entries(acl: &mut Vec<DatabaseAclEntry>) {
344 acl.retain(|entry| !entry.privileges.is_empty() || !entry.grant_options.is_empty());
345}
346
347#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
348pub struct DatabasePrivileges {
349 pub connect: bool,
350 pub create: bool,
351 pub temporary: bool,
352}
353
354impl DatabasePrivileges {
355 pub const ALL: Self = Self {
356 connect: true,
357 create: true,
358 temporary: true,
359 };
360
361 pub const fn intersects(self, other: Self) -> bool {
362 (self.connect && other.connect)
363 || (self.create && other.create)
364 || (self.temporary && other.temporary)
365 }
366
367 pub fn insert(&mut self, other: Self) {
368 self.connect |= other.connect;
369 self.create |= other.create;
370 self.temporary |= other.temporary;
371 }
372
373 pub fn remove(&mut self, other: Self) {
374 self.connect &= !other.connect;
375 self.create &= !other.create;
376 self.temporary &= !other.temporary;
377 }
378
379 pub const fn is_empty(self) -> bool {
380 !self.connect && !self.create && !self.temporary
381 }
382}
383
384#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
385pub struct DatabaseAclEntry {
386 pub role: uqa_core::catalog_acl::AclGrantee,
387 pub grantor: Option<String>,
388 pub privileges: DatabasePrivileges,
389 pub grant_options: DatabasePrivileges,
390}
391
392#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
393pub struct DatabaseSecurity {
394 pub role_owner: String,
395 pub acl: Option<Vec<DatabaseAclEntry>>,
396}
397
398impl DatabaseSecurity {
399 pub fn bootstrap() -> Self {
400 Self {
401 role_owner: "uqa".into(),
402 acl: None,
403 }
404 }
405}
406
407pub fn resolve_database_grant_targets(databases: &[String]) -> Result<(), SQLError> {
408 for database in databases {
409 if database != DATABASE_NAME {
410 return Err(SQLError::Routine {
411 sqlstate: "3D000".into(),
412 message: format!("database \"{database}\" does not exist"),
413 });
414 }
415 }
416 Ok(())
417}
418
419pub fn apply_database_acl(
420 statement: &GrantDatabaseStmt,
421 grantees: &[AclGrantee],
422 privileges: &[DatabaseAclPrivilege],
423 current_user: &(impl RoleSubject + ?Sized),
424 roles: &BTreeMap<String, RoleDefinition>,
425 memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
426 current: &DatabaseSecurity,
427) -> Result<(DatabaseSecurity, usize), SQLError> {
428 let grantors = privileges
429 .iter()
430 .map(|privilege| {
431 (
432 *privilege,
433 select_acl_grantor(current, *privilege, current_user, roles, memberships),
434 )
435 })
436 .collect::<Vec<_>>();
437 let grantable = grantors
438 .iter()
439 .filter(|(_, grantor)| grantor.is_some())
440 .count();
441 let mut next = current.clone();
442 for (privilege, grantor) in grantors {
443 let Some(grantor) = grantor else {
444 continue;
445 };
446 if statement.is_grant {
447 grant_acl(
448 &mut next,
449 privilege,
450 grantees,
451 &grantor,
452 statement.grant_option,
453 );
454 } else {
455 revoke_acl(
456 &mut next,
457 privilege,
458 grantees,
459 &grantor,
460 statement.grant_option_only,
461 statement.revoke_behavior == DatabaseRevokeBehavior::Cascade,
462 )?;
463 }
464 }
465 Ok((next, grantable))
466}
467
468pub fn validate_database_acl_roles(
469 statement: &GrantDatabaseStmt,
470 grantees: &[AclGrantee],
471 requested_grantor: Option<&str>,
472 current_user: &(impl RoleSubject + ?Sized),
473 roles: &BTreeMap<String, RoleDefinition>,
474) -> Result<(), SQLError> {
475 for role in grantees {
476 if role
477 .role_name()
478 .is_some_and(|name| !roles.contains_key(name))
479 {
480 return Err(SQLError::Routine {
481 sqlstate: "42704".into(),
482 message: format!("role \"{role}\" does not exist"),
483 });
484 }
485 }
486 if statement.is_grant && statement.grant_option && grantees.iter().any(AclGrantee::is_public) {
487 return Err(SQLError::Routine {
488 sqlstate: "0LP01".into(),
489 message: "grant options can only be granted to roles".into(),
490 });
491 }
492 if let Some(requested_grantor) = requested_grantor {
493 if !roles.contains_key(requested_grantor) {
494 return Err(SQLError::Routine {
495 sqlstate: "42704".into(),
496 message: format!("role \"{requested_grantor}\" does not exist"),
497 });
498 }
499 if current_user.role_name(roles) != Some(requested_grantor) {
500 return Err(SQLError::Routine {
501 sqlstate: "0A000".into(),
502 message: "grantor must be current user".into(),
503 });
504 }
505 }
506 Ok(())
507}
508
509pub fn database_acl_warning(is_grant: bool, partial: bool, name: &str) -> (&'static str, String) {
510 let message = match (is_grant, partial) {
511 (true, true) => format!("not all privileges were granted for \"{name}\""),
512 (true, false) => format!("no privileges were granted for \"{name}\""),
513 (false, true) => format!("not all privileges could be revoked for \"{name}\""),
514 (false, false) => format!("no privileges could be revoked for \"{name}\""),
515 };
516 ("WARNING", message)
517}
518
519pub fn validate_stored_database_security(
520 security: &DatabaseSecurity,
521 roles: &BTreeMap<String, RoleDefinition>,
522) -> Result<(), String> {
523 if !roles.contains_key(&security.role_owner) {
524 return Err(format!(
525 "persisted database owner `{}` does not exist",
526 security.role_owner
527 ));
528 }
529 if let Some(acl) = security.acl.as_ref() {
530 for entry in acl {
531 let grantor = entry.grantor.as_deref().unwrap_or(&security.role_owner);
532 if (entry
533 .role
534 .role_name()
535 .is_some_and(|name| !roles.contains_key(name)))
536 || !roles.contains_key(grantor)
537 {
538 return Err(format!(
539 "persisted database ACL `{}` from `{grantor}` references a missing role",
540 entry.role
541 ));
542 }
543 }
544 }
545 Ok(())
546}