uqa_sql/catalog/security/
database_inquiry.rs1use super::database::{
10 parse_privilege_checks, role_has_database_privilege, role_has_database_privilege_check,
11 DatabaseAclPrivilege, DatabaseSecurity,
12};
13use crate::{
14 catalog::{
15 roles::{guards::RoleCatalogGuards, RoleDefinition, RoleReferenceNames},
16 DATABASE_NAME, DATABASE_OID,
17 },
18 SQLError,
19};
20use std::collections::BTreeMap;
21use uqa_core::Value;
22
23pub type DatabaseSecurityRead<'a> = Box<dyn std::ops::Deref<Target = DatabaseSecurity> + 'a>;
24
25pub trait DatabasePrivilegeCatalog {
26 fn refresh_privilege_catalog(&self) -> Result<(), SQLError>;
27 fn security(&self) -> DatabaseSecurityRead<'_>;
28}
29
30pub struct DatabasePrivilegeInquiry<'a> {
31 pub catalog: &'a dyn DatabasePrivilegeCatalog,
32 pub names: &'a dyn RoleReferenceNames,
33 pub roles: &'a dyn RoleCatalogGuards,
34}
35
36impl DatabasePrivilegeInquiry<'_> {
37 pub fn ensure_database_privilege(
38 &self,
39 role: &str,
40 privilege: DatabaseAclPrivilege,
41 ) -> Result<(), SQLError> {
42 if role_has_database_privilege(
43 &self.catalog.security(),
44 role,
45 privilege,
46 &self.roles.role_definitions(),
47 &self.roles.role_memberships(),
48 ) {
49 return Ok(());
50 }
51 let message = match privilege {
52 DatabaseAclPrivilege::Temporary => {
53 format!(
54 "permission denied to create temporary tables in database \"{DATABASE_NAME}\""
55 )
56 }
57 DatabaseAclPrivilege::Connect | DatabaseAclPrivilege::Create => {
58 format!("permission denied for database {DATABASE_NAME}")
59 }
60 };
61 Err(SQLError::Routine {
62 sqlstate: "42501".into(),
63 message,
64 })
65 }
66
67 pub fn has_database_privilege_value(&self, arguments: &[Value]) -> Result<Value, SQLError> {
68 if arguments.iter().any(|argument| argument == &Value::Null) {
69 return Ok(Value::Null);
70 }
71 self.catalog.refresh_privilege_catalog()?;
72 let (subject_value, database_value, privilege_value) = match arguments {
73 [database, privilege] => (None, database, privilege),
74 [subject, database, privilege] => (Some(subject), database, privilege),
75 _ => {
76 return Err(SQLError::BadArity {
77 name: "has_database_privilege".into(),
78 expected: "2 or 3".into(),
79 actual: arguments.len(),
80 })
81 }
82 };
83 let current_user = subject_value
84 .is_none()
85 .then(|| self.names.current_user_name());
86 let subject = {
87 let roles = self.roles.role_definitions();
88 subject_value.map_or_else(
89 || Ok(current_user),
90 |value| resolve_database_privilege_role(value, &roles),
91 )?
92 };
93 let database_exists = resolve_database_privilege_target(database_value)?;
94 let privilege = match privilege_value {
95 Value::Str(privilege) | Value::FixedChar(privilege) => privilege,
96 other => {
97 return Err(SQLError::TypeMismatch(format!(
98 "has_database_privilege privilege must be text, got {other:?}"
99 )))
100 }
101 };
102 let checks = parse_privilege_checks(privilege)?;
103 let roles = self.roles.role_definitions();
104 let memberships = self.roles.role_memberships();
105 let subject_is_superuser = subject.as_ref().is_some_and(|subject| {
106 roles
107 .get(subject)
108 .is_some_and(|role| role.has(crate::ast::RoleAttribute::Superuser))
109 });
110 if !database_exists {
111 return if subject_is_superuser {
112 Ok(Value::Bool(true))
113 } else {
114 Ok(Value::Null)
115 };
116 }
117 let Some(subject) = subject else {
118 return Ok(Value::Bool(false));
119 };
120 let security = self.catalog.security();
121 Ok(Value::Bool(checks.into_iter().any(|check| {
122 role_has_database_privilege_check(&security, &subject, check, &roles, &memberships)
123 })))
124 }
125}
126
127fn resolve_database_privilege_target(value: &Value) -> Result<bool, SQLError> {
128 match value {
129 Value::Str(name) | Value::FixedChar(name) => {
130 if name == DATABASE_NAME {
131 Ok(true)
132 } else {
133 Err(SQLError::Routine {
134 sqlstate: "3D000".into(),
135 message: format!("database \"{name}\" does not exist"),
136 })
137 }
138 }
139 Value::Int(oid) => Ok(*oid == DATABASE_OID),
140 other => Err(SQLError::TypeMismatch(format!(
141 "has_database_privilege database must be text or oid, got {other:?}"
142 ))),
143 }
144}
145
146fn resolve_database_privilege_role(
147 value: &Value,
148 roles: &BTreeMap<String, RoleDefinition>,
149) -> Result<Option<String>, SQLError> {
150 match value {
151 Value::Str(name) | Value::FixedChar(name) => {
152 if roles.contains_key(name) {
153 Ok(Some(name.clone()))
154 } else {
155 Err(SQLError::Routine {
156 sqlstate: "42704".into(),
157 message: format!("role \"{name}\" does not exist"),
158 })
159 }
160 }
161 Value::Int(oid) => Ok(roles
162 .values()
163 .find(|role| role.oid == *oid)
164 .map(|role| role.name.clone())),
165 other => Err(SQLError::TypeMismatch(format!(
166 "has_database_privilege role must be name or oid, got {other:?}"
167 ))),
168 }
169}