1use plsql_catalog::{CatalogObject, CatalogSnapshot, GrantPrivilege, Grantee, SchemaCatalog};
2use plsql_core::{
3 Confidence, ConfidenceLevel, Evidence, ObjectName, RoleName, SchemaName, UnknownReason,
4};
5use tracing::instrument;
6
7use crate::{
8 AccessControlEntry, AuthorizationAmbiguity, AuthorizationMode, CrossSchemaWrite, GrantOption,
9 PrivilegeConfig, PrivilegeModel, ResolvedPrivilege, SynonymPrivilegePath,
10};
11
12#[instrument(level = "debug", skip_all)]
14pub fn resolve_privileges(snapshot: &CatalogSnapshot, config: &PrivilegeConfig) -> PrivilegeModel {
15 let mut model = PrivilegeModel::default();
16
17 for (schema_name, schema_catalog) in &snapshot.schemas {
18 resolve_grants_for_schema(schema_name, schema_catalog, config, &mut model);
19 resolve_access_control_for_schema(schema_name, schema_catalog, &mut model);
20 resolve_cross_schema_writes(schema_catalog, config, &mut model);
21 resolve_synonym_paths(schema_name, schema_catalog, &mut model);
22 }
23
24 model.public_grants = model
25 .privileges
26 .iter()
27 .filter(|p| matches!(p.grantee, Grantee::Public))
28 .cloned()
29 .collect();
30
31 model
32}
33
34pub fn authorization_mode_for_object(
36 schema_catalog: &SchemaCatalog,
37 object_name: &ObjectName,
38) -> Option<AuthorizationMode> {
39 match schema_catalog.objects.get(object_name)? {
40 CatalogObject::Package(pkg) => pkg.authid_current_user.map(|invoker| {
41 if invoker {
42 AuthorizationMode::Invoker
43 } else {
44 AuthorizationMode::Definer
45 }
46 }),
47 CatalogObject::Procedure(proc) => proc.signature.authid_current_user.map(|invoker| {
48 if invoker {
49 AuthorizationMode::Invoker
50 } else {
51 AuthorizationMode::Definer
52 }
53 }),
54 CatalogObject::Function(func) => func.signature.authid_current_user.map(|invoker| {
55 if invoker {
56 AuthorizationMode::Invoker
57 } else {
58 AuthorizationMode::Definer
59 }
60 }),
61 _ => Some(AuthorizationMode::Definer),
62 }
63}
64
65fn resolve_grants_for_schema(
66 schema_name: &SchemaName,
67 schema_catalog: &SchemaCatalog,
68 config: &PrivilegeConfig,
69 model: &mut PrivilegeModel,
70) {
71 for grant in &schema_catalog.grants {
72 let (confidence, via_role) = grant_confidence(&grant.grantee, config);
73
74 model.privileges.push(ResolvedPrivilege {
75 object_owner: grant.object_owner,
76 object_name: grant.object_name,
77 privilege: grant.privilege,
78 grantee: grant.grantee.clone(),
79 grant_option: if grant.grantable {
80 GrantOption::Grantable
81 } else if grant.with_hierarchy {
82 GrantOption::Hierarchy
83 } else {
84 GrantOption::None
85 },
86 via_role,
87 confidence,
88 evidence: Evidence::new(
89 "privilege-grant",
90 format!(
91 "Grant {:?} on {:?}.{:?} to {:?}",
92 grant.privilege, schema_name, grant.object_name, grant.grantee
93 ),
94 ),
95 });
96
97 if let Grantee::Role(role) = &grant.grantee {
98 if !config.enabled_roles.contains(role) {
99 model.runtime_ambiguities.push(AuthorizationAmbiguity {
100 schema: grant.object_owner,
101 object: grant.object_name,
102 reason: UnknownReason::RuntimeGrantOrRole,
103 dependent_roles: vec![*role],
104 evidence: Evidence::new(
105 "runtime-role-ambiguity",
106 format!("Grant to role {:?} may not be enabled at runtime", role),
107 ),
108 });
109 }
110 }
111 }
112}
113
114fn resolve_access_control_for_schema(
115 schema_name: &SchemaName,
116 schema_catalog: &SchemaCatalog,
117 model: &mut PrivilegeModel,
118) {
119 for obj in schema_catalog.objects.values() {
120 let accessible_by = match obj {
121 CatalogObject::Package(pkg) => pkg.accessible_by.clone(),
122 CatalogObject::Procedure(proc) => proc.signature.accessible_by.clone(),
123 CatalogObject::Function(func) => func.signature.accessible_by.clone(),
124 _ => vec![],
125 };
126
127 if !accessible_by.is_empty() {
128 model.access_control.push(AccessControlEntry {
129 declaring_schema: *schema_name,
130 declaring_object: object_name_for(obj),
131 allowed_callers: accessible_by,
132 });
133 }
134 }
135}
136
137fn resolve_cross_schema_writes(
138 schema_catalog: &SchemaCatalog,
139 config: &PrivilegeConfig,
140 model: &mut PrivilegeModel,
141) {
142 for grant in &schema_catalog.grants {
143 if !matches!(
144 grant.privilege,
145 GrantPrivilege::Insert | GrantPrivilege::Update | GrantPrivilege::Delete
146 ) {
147 continue;
148 }
149
150 if let Grantee::User(user) = &grant.grantee {
151 let gs = SchemaName::new(user.symbol());
152 if gs != grant.object_owner {
153 let (confidence, runtime_ambiguity) = write_ambiguity(&grant.grantee, config);
154
155 model.cross_schema_writes.push(CrossSchemaWrite {
156 caller_schema: gs,
157 caller_object: ObjectName::new(user.symbol()),
158 target_schema: grant.object_owner,
159 target_object: grant.object_name,
160 privilege: grant.privilege,
161 confidence,
162 evidence: Evidence::new(
163 "cross-schema-write",
164 format!(
165 "Write grant {:?} on {:?}.{:?} to {:?}",
166 grant.privilege, grant.object_owner, grant.object_name, user
167 ),
168 ),
169 runtime_ambiguity,
170 });
171 }
172 }
173 }
174}
175
176fn resolve_synonym_paths(
177 schema_name: &SchemaName,
178 schema_catalog: &SchemaCatalog,
179 model: &mut PrivilegeModel,
180) {
181 for (syn_name, syn_target) in &schema_catalog.synonyms {
182 let target_schema = syn_target.target_owner.unwrap_or(*schema_name);
183
184 model.synonym_paths.push(SynonymPrivilegePath {
185 synonym_schema: *schema_name,
186 synonym_name: ObjectName::new(syn_name.symbol()),
187 target_schema,
188 target_object: syn_target.target_name,
189 is_public: syn_target.public_synonym,
190 confidence: Confidence::new(
191 ConfidenceLevel::Medium,
192 Some("Synonym target can change at runtime".to_string()),
193 ),
194 });
195 }
196}
197
198fn grant_confidence(grantee: &Grantee, config: &PrivilegeConfig) -> (Confidence, Option<RoleName>) {
199 match grantee {
200 Grantee::User(_) => (Confidence::new(ConfidenceLevel::High, None), None),
201 Grantee::Role(role) => {
202 if config.enabled_roles.contains(role) {
203 (
204 Confidence::new(
205 ConfidenceLevel::High,
206 Some(format!("Role {:?} is enabled in profile", role)),
207 ),
208 Some(*role),
209 )
210 } else {
211 (
212 Confidence::new(
213 ConfidenceLevel::Low,
214 Some(format!("Role {:?} may not be enabled at runtime", role)),
215 ),
216 Some(*role),
217 )
218 }
219 }
220 Grantee::Public => (
221 Confidence::new(ConfidenceLevel::High, Some("PUBLIC grant".to_string())),
222 None,
223 ),
224 }
225}
226
227fn write_ambiguity(
228 grantee: &Grantee,
229 config: &PrivilegeConfig,
230) -> (Confidence, Option<UnknownReason>) {
231 match grantee {
232 Grantee::Role(role) => {
233 if config.enabled_roles.contains(role) {
234 (Confidence::new(ConfidenceLevel::High, None), None)
235 } else {
236 (
237 Confidence::new(
238 ConfidenceLevel::Low,
239 Some(format!("Role {:?} may not be active", role)),
240 ),
241 Some(UnknownReason::RuntimeGrantOrRole),
242 )
243 }
244 }
245 _ => (Confidence::new(ConfidenceLevel::High, None), None),
246 }
247}
248
249fn object_name_for(obj: &CatalogObject) -> ObjectName {
250 match obj {
251 CatalogObject::Table(t) => t.common.name,
252 CatalogObject::View(v) => v.common.name,
253 CatalogObject::MaterializedView(m) => m.common.name,
254 CatalogObject::Sequence(s) => s.common.name,
255 CatalogObject::Type(t) => t.common.name,
256 CatalogObject::Package(p) => p.common.name,
257 CatalogObject::Procedure(p) => p.common.name,
258 CatalogObject::Function(f) => f.common.name,
259 CatalogObject::Trigger(t) => t.common.name,
260 CatalogObject::SchedulerJob(j) => j.common.name,
261 CatalogObject::EditioningView(e) => e.common.name,
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use plsql_catalog::{ObjectCommon, ObjectType, PackageMetadata, SchemaCatalog};
269 use plsql_core::SymbolId;
270 use std::collections::HashMap;
271
272 fn make_schema_name(s: &str) -> SchemaName {
273 SchemaName::new(SymbolId::new(s.len() as u64))
274 }
275
276 fn make_object_name(s: &str) -> ObjectName {
277 ObjectName::new(SymbolId::new(s.len() as u64 + 100))
278 }
279
280 #[test]
281 fn test_empty_catalog_produces_empty_model() {
282 let snapshot = CatalogSnapshot {
283 schemas: HashMap::new(),
284 ..CatalogSnapshot::default()
285 };
286 let config = PrivilegeConfig::default();
287 let model = resolve_privileges(&snapshot, &config);
288 assert!(model.privileges.is_empty());
289 assert!(model.public_grants.is_empty());
290 assert!(model.access_control.is_empty());
291 assert!(model.cross_schema_writes.is_empty());
292 assert!(model.synonym_paths.is_empty());
293 assert!(model.runtime_ambiguities.is_empty());
294 }
295
296 #[test]
297 fn test_definer_vs_invoker_authorization() {
298 use plsql_catalog::CatalogObject;
299
300 let owner = make_schema_name("OWNER");
301 let pkg_name = make_object_name("MY_PKG");
302
303 let mut schema = SchemaCatalog::default();
305 schema.objects.insert(
306 pkg_name,
307 CatalogObject::Package(PackageMetadata {
308 common: ObjectCommon {
309 owner,
310 name: pkg_name,
311 object_type: ObjectType::Package,
312 ..ObjectCommon::default()
313 },
314 authid_current_user: Some(false),
315 ..PackageMetadata::default()
316 }),
317 );
318
319 let mode = authorization_mode_for_object(&schema, &pkg_name);
320 assert_eq!(mode, Some(AuthorizationMode::Definer));
321
322 if let Some(CatalogObject::Package(pkg)) = schema.objects.get_mut(&pkg_name) {
324 pkg.authid_current_user = Some(true);
325 }
326 let mode = authorization_mode_for_object(&schema, &pkg_name);
327 assert_eq!(mode, Some(AuthorizationMode::Invoker));
328 }
329
330 #[test]
331 fn authorization_mode_unknown_authid_nonroutine_and_absent() {
332 use plsql_catalog::{CatalogObject, TableMetadata};
333
334 let owner = make_schema_name("OWNER");
335 let pkg_name = make_object_name("UNK_PKG");
336 let tbl_name = make_object_name("SOME_TBL");
337 let absent = make_object_name("NOPE");
338
339 let mut schema = SchemaCatalog::default();
340
341 schema.objects.insert(
347 pkg_name,
348 CatalogObject::Package(PackageMetadata {
349 common: ObjectCommon {
350 owner,
351 name: pkg_name,
352 object_type: ObjectType::Package,
353 ..ObjectCommon::default()
354 },
355 authid_current_user: None,
356 ..PackageMetadata::default()
357 }),
358 );
359 schema
362 .objects
363 .insert(tbl_name, CatalogObject::Table(TableMetadata::default()));
364
365 assert_eq!(
366 authorization_mode_for_object(&schema, &pkg_name),
367 None,
368 "unknown AUTHID must surface as None (R13), never silently Definer"
369 );
370 assert_eq!(
371 authorization_mode_for_object(&schema, &tbl_name),
372 Some(AuthorizationMode::Definer),
373 "non-routine objects resolve to Definer"
374 );
375 assert_eq!(
376 authorization_mode_for_object(&schema, &absent),
377 None,
378 "object absent from the catalog resolves to None"
379 );
380 }
381}