fix(derived): refuse dependencies on inactive rows, and keep the RLS scope guard coherent

The graph now holds only effective active rows, and a declared dependency that
resolves to an inactive row (existing, or one this submission deactivates) is
rejected: the engine reads only active rows and aborts every read of the table
on the unresolved reference.

The derived-variable / RLS guard goes back to rejecting every scope. Exempting
VIEW rules left them to fail a moment later in mp_filtercheck, which validates
against the physical table where the column does not exist, so the pairing is
refused up front with a message that says why.

RLS batches now exclude delete-marked rows, matching the badloadtypes check, so
a rule being removed is not validated against the derived configuration.
This commit is contained in:
dc committed 2026-10-23 09:44:32 +00:00
1 parent 85c89ca2a4
commit 7799a300df
2 files changed
+56 -16

No files matched your search

@@ -154,7 +154,9 @@ run;
/**
* The graph has to hold for the resulting configuration: the incoming rows
* plus the current rows of the same tables that this submission does not
* replace.
* replace. Only active rows are part of it - the engine reads nothing else
* (mpe_derived.sas), so an inactive row can neither resolve a dependency nor
* be resolved by one.
*/
proc sql;
create table work.derived_rows as
@@ -163,13 +165,12 @@ create table work.derived_rows as
,strip(var_logic) as logic
from work.staging_ds
where upcase(_____DELETE__THIS__RECORD_____) ne 'YES'
and active=1
union all
select libref, dsn, upcase(strip(var_nm)) as name
,upcase(strip(var_deps)) as deps
,strip(var_logic) as logic
from &DC_LIBREF..mpe_derived
/* only active rows are read at run time (mpe_derived.sas), so an inactive
row must not enter the dependency graph */
where &dc_dttmtfmt. lt tx_to
and active=1
and catx('.',libref,dsn,var_nm) not in
@@ -188,6 +189,46 @@ data work.derived_dep;
keep libref dsn name dep_nm;
run;
/* a dependency on a row the engine will not read cannot resolve: it aborts
every read of the table with "VAR_DEPS lists X, which is not a column of the
table or a derived variable". Catch it here, including a row this submission
deactivates, which the control table alone does not show yet. */
proc sql;
create table work.derived_baddep as
select d.libref, d.dsn, d.name, d.dep_nm
from work.derived_dep d
inner join (
select upcase(strip(libref)) as libref
,upcase(strip(dsn)) as dsn
,upcase(strip(var_nm)) as name
from work.staging_ds
where upcase(_____DELETE__THIS__RECORD_____) ne 'YES'
and active ne 1
union all
select upcase(strip(libref)) as libref
,upcase(strip(dsn)) as dsn
,upcase(strip(var_nm)) as name
from &DC_LIBREF..mpe_derived
where &dc_dttmtfmt. lt tx_to
and active ne 1
) inactive
on inactive.libref=d.libref
and inactive.dsn=d.dsn
and inactive.name=d.dep_nm;
quit;
data _null_;
set work.derived_baddep(obs=1);
call symputx('errmsg'
,cats('MPE_DERIVED: ',name,': VAR_DEPS lists ',dep_nm
,', which is not active - the engine cannot resolve it'));
run;
%mp_abort(iftrue= (0 < %mf_nobs(work.derived_baddep))
,mac=mpe_derived_postedit
,msg=%superq(errmsg)
)
data work.derived_ref;
set work.derived_rows;
length l $4000 tok $32;
@@ -273,9 +314,10 @@ run;
)
/* the same invariant the RLS hook enforces from its side: a derived variable
cannot be named by an EDIT or ALL row level security rule, because the
columns are dropped before the security clause is evaluated. A VIEW rule is
applied on the view the EDIT screen reads, where the column exists. */
cannot be named by an active row level security rule of any scope. Only the
VIEW scope could evaluate it (on the view the EDIT screen reads), but the RLS
hook validates every rule against the physical table, where the column does
not exist, so the pairing is refused up front rather than failing there. */
proc sql;
create table work.derived_rls as
select d.libref, d.dsn, d.name
@@ -285,7 +327,6 @@ create table work.derived_rls as
and upcase(r.rls_table)=upcase(d.dsn)
and upcase(r.rls_variable_nm)=upcase(d.name)
where r.rls_active=1
and upcase(r.rls_scope) in ('EDIT','ALL')
and &dc_dttmtfmt lt r.tx_to;
quit;
@@ -55,13 +55,11 @@ run;
) change the loadtype.)
)
/* group and subgroup are not used for validation; scope is carried for the
derived-variable check below */
/* group and subgroup are not used for validation */
proc sql;
create table work.batches as
select distinct upcase(rls_libref) as rls_libref,
upcase(rls_table) as rls_table,
upcase(rls_scope) as rls_scope,
rls_group_logic as group_logic,
rls_subgroup_logic as subgroup_logic,
rls_subgroup_id as subgroup_id,
@@ -70,6 +68,7 @@ create table work.batches as
rls_raw_value as raw_value
from work.staging_ds
where rls_active=1
and upcase(_____DELETE__THIS__RECORD_____) ne 'YES'
order by rls_libref, rls_table;
%let cnt=0;
@@ -84,10 +83,11 @@ data _null_;
end;
run;
/* derived variables exist only in the view the EDIT screen reads - an EDIT or
ALL rule on one cannot be evaluated against the submitted data, so reject it
here rather than let the submit fail later with a variable-not-found error.
A VIEW rule is applied on that view, where the column exists. */
/* derived variables exist only in the view the EDIT screen reads - a rule on
one cannot be validated against the physical table, so reject it here rather
than let mp_filtercheck fail with a variable-not-found error. Applies to
every scope: the VIEW scope could evaluate the column, but this hook checks
rules against the table they are submitted for. */
proc sql;
create table work.rls_derived as
select b.rls_libref, b.rls_table, b.variable_nm
@@ -96,8 +96,7 @@ create table work.rls_derived as
on upcase(d.libref)=b.rls_libref
and upcase(d.dsn)=b.rls_table
and upcase(d.var_nm)=upcase(b.variable_nm)
where b.rls_scope in ('EDIT','ALL')
and &dc_dttmtfmt lt d.tx_to
where &dc_dttmtfmt lt d.tx_to
and d.active=1;
quit;