feat: validation checks to prevent incompatible RLS rules (eg REPLACE load type). Closes #211
This commit is contained in:
@@ -171,6 +171,15 @@ run;
|
||||
|
||||
If even one submitted row falls outside the user's permitted row set, the entire staging request is aborted before any approval/apply step.
|
||||
|
||||
## Incompatibility with REPLACE Load Type
|
||||
|
||||
RLS with `EDIT` scope is **incompatible** with tables configured with `LOAD_TYPE=REPLACE` in `MPE_TABLES`. A REPLACE load wipes and reloads the entire target table, so row-level filtering of submitted records cannot be enforced meaningfully (the rows a user is *not* allowed to see would also be deleted). Backend validations therefore abort in both directions (see [issue #211](https://git.datacontroller.io/dc/dc/issues/211)):
|
||||
|
||||
1. **`mpe_row_level_security_postedit.sas`** — aborts when activating a rule with `RLS_SCOPE in ('EDIT','ALL')` against a table whose current `MPE_TABLES` record has `LOADTYPE='REPLACE'`.
|
||||
2. **`mpe_tables_postedit.sas`** — aborts when setting `LOADTYPE='REPLACE'` on a table that already has active, current `RLS_SCOPE in ('EDIT','ALL')` rules in `MPE_ROW_LEVEL_SECURITY`.
|
||||
|
||||
`VIEW`-scope rules remain compatible with REPLACE loads, since they only affect read paths.
|
||||
|
||||
## Edit-Time Validation (Injection Defence)
|
||||
|
||||
Because `RLS_RAW_VALUE` is free text that ends up inside a generated WHERE clause, it is a potential SAS code-injection vector. Mitigations:
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
@file
|
||||
@brief testing stagedata RLS / REPLACE loadtype incompatibility (#211)
|
||||
@details Backend validations should abort when:
|
||||
1. loading row level EDIT security against a table with LOADTYPE=REPLACE
|
||||
2. applying LOADTYPE=REPLACE to a table with row level EDIT security
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mx_testservice.sas
|
||||
@li mp_assert.sas
|
||||
|
||||
|
||||
**/
|
||||
|
||||
%let _program=&appLoc/services/editors/stagedata;
|
||||
|
||||
/**
|
||||
* Prep - register a DCTEST table with REPLACE loadtype (direct insert,
|
||||
* bypassing stagedata) and create the physical table
|
||||
*/
|
||||
data dctest.dc_replace;
|
||||
x=1;
|
||||
run;
|
||||
proc sql noprint;
|
||||
delete from &dc_libref..mpe_tables where libref="DCTEST" and dsn='DC_REPLACE';
|
||||
insert into &dc_libref..mpe_tables
|
||||
set tx_from=0
|
||||
,tx_to='31DEC5999:23:59:59'dt
|
||||
,libref="DCTEST"
|
||||
,dsn='DC_REPLACE'
|
||||
,buskey='X'
|
||||
,loadtype='REPLACE'
|
||||
,num_of_approvals_required=1;
|
||||
quit;
|
||||
|
||||
/**
|
||||
* Test 1 - submitting an RLS EDIT rule against a REPLACE table should abort
|
||||
*/
|
||||
data work.sascontroltable;
|
||||
action='LOAD';
|
||||
message='RLS EDIT rule on REPLACE table should abort';
|
||||
libds="&dc_libref..MPE_ROW_LEVEL_SECURITY";
|
||||
output;
|
||||
stop;
|
||||
run;
|
||||
|
||||
data work.jsdata;
|
||||
length RLS_SCOPE $8 RLS_GROUP $128 RLS_LIBREF $8 RLS_TABLE $32
|
||||
RLS_GROUP_LOGIC $3 RLS_SUBGROUP_LOGIC $3 RLS_VARIABLE_NM $32
|
||||
RLS_OPERATOR_NM $12 RLS_RAW_VALUE $4000
|
||||
_____DELETE__THIS__RECORD_____ $3;
|
||||
RLS_SCOPE='EDIT';
|
||||
RLS_GROUP='SASAdministrators';
|
||||
RLS_LIBREF='DCTEST';
|
||||
RLS_TABLE='DC_REPLACE';
|
||||
RLS_GROUP_LOGIC='AND';
|
||||
RLS_SUBGROUP_LOGIC='OR';
|
||||
RLS_SUBGROUP_ID=0;
|
||||
RLS_VARIABLE_NM='X';
|
||||
RLS_OPERATOR_NM='NE';
|
||||
RLS_RAW_VALUE='1';
|
||||
RLS_ACTIVE=1;
|
||||
_____DELETE__THIS__RECORD_____='No';
|
||||
output;
|
||||
run;
|
||||
|
||||
%mx_testservice(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputdatasets=work.sascontroltable work.jsdata,
|
||||
outlib=web1,
|
||||
outref=wb1,
|
||||
mdebug=&sasjs_mdebug
|
||||
)
|
||||
|
||||
%let abort1=0;
|
||||
data _null_;
|
||||
infile wb1;
|
||||
input;
|
||||
putlog _infile_;
|
||||
if index(_infile_,'sasjsAbort')
|
||||
and index(_infile_,'REPLACE loadtype')
|
||||
then call symputx('abort1',1);
|
||||
run;
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&abort1=1),
|
||||
desc=Checking RLS EDIT rule is rejected for REPLACE loadtype table (#211),
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/**
|
||||
* Test 2 - applying REPLACE loadtype to a table with RLS EDIT security
|
||||
* should abort. First insert an active EDIT rule directly.
|
||||
*/
|
||||
proc sql noprint;
|
||||
delete from &dc_libref..mpe_row_level_security
|
||||
where rls_libref="DCTEST" and rls_table='WIDEBOY';
|
||||
select coalesce(max(rls_rk),0)+1 into: rlsrk
|
||||
from &dc_libref..mpe_row_level_security;
|
||||
insert into &dc_libref..mpe_row_level_security
|
||||
set tx_from=0
|
||||
,tx_to='31DEC5999:23:59:59'dt
|
||||
,rls_rk=&rlsrk
|
||||
,rls_scope='EDIT'
|
||||
,rls_group='SASAdministrators'
|
||||
,rls_libref='DCTEST'
|
||||
,rls_table='WIDEBOY'
|
||||
,rls_group_logic='AND'
|
||||
,rls_subgroup_logic='OR'
|
||||
,rls_subgroup_id=0
|
||||
,rls_variable_nm='ROW_ID'
|
||||
,rls_operator_nm='NE'
|
||||
,rls_raw_value='1'
|
||||
,rls_active=1;
|
||||
quit;
|
||||
|
||||
/* now stage a REPLACE loadtype for DCTEST.WIDEBOY */
|
||||
data work.sascontroltable;
|
||||
action='LOAD';
|
||||
message='REPLACE loadtype on RLS-secured table should abort';
|
||||
libds="&dc_libref..MPE_TABLES";
|
||||
output;
|
||||
stop;
|
||||
run;
|
||||
|
||||
data work.jsdata;
|
||||
set &dc_libref..mpe_tables(where=(libref='DCTEST' and dsn='WIDEBOY'));
|
||||
loadtype='REPLACE';
|
||||
length _____DELETE__THIS__RECORD_____ $3;
|
||||
_____DELETE__THIS__RECORD_____='No';
|
||||
run;
|
||||
|
||||
%mx_testservice(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputdatasets=work.sascontroltable work.jsdata,
|
||||
outlib=web2,
|
||||
outref=wb2,
|
||||
mdebug=&sasjs_mdebug
|
||||
)
|
||||
|
||||
%let abort2=0;
|
||||
data _null_;
|
||||
infile wb2;
|
||||
input;
|
||||
putlog _infile_;
|
||||
if index(_infile_,'sasjsAbort')
|
||||
and index(_infile_,'row level EDIT security')
|
||||
then call symputx('abort2',1);
|
||||
run;
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&abort2=1),
|
||||
desc=Checking REPLACE loadtype is rejected for RLS EDIT-secured table (#211),
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/**
|
||||
* Test 3 - sanity check: REPLACE loadtype on an unsecured table is fine
|
||||
*/
|
||||
data work.jsdata;
|
||||
set &dc_libref..mpe_tables(where=(libref='DCTEST' and dsn='WIDEBOY'));
|
||||
loadtype='UPDATE';
|
||||
length _____DELETE__THIS__RECORD_____ $3;
|
||||
_____DELETE__THIS__RECORD_____='No';
|
||||
run;
|
||||
|
||||
/* remove the RLS rule first (cleanup of test 2) */
|
||||
proc sql noprint;
|
||||
delete from &dc_libref..mpe_row_level_security
|
||||
where rls_libref="DCTEST" and rls_table='WIDEBOY';
|
||||
quit;
|
||||
|
||||
%mx_testservice(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputdatasets=work.sascontroltable work.jsdata,
|
||||
outlib=web3,
|
||||
mdebug=&sasjs_mdebug
|
||||
)
|
||||
|
||||
%let status3=0;
|
||||
data _null_;
|
||||
set web3.sasparams;
|
||||
putlog (_all_)(=);
|
||||
if status='SUCCESS' then call symputx('status3',1);
|
||||
run;
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&status3=1),
|
||||
desc=Checking non-REPLACE loadtype still stages successfully after cleanup,
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/* final cleanup - remove the REPLACE table registration */
|
||||
proc sql noprint;
|
||||
delete from &dc_libref..mpe_tables where libref="DCTEST" and dsn='DC_REPLACE';
|
||||
quit;
|
||||
@@ -13,8 +13,12 @@
|
||||
This validation checks the incoming row_level_security settings to ensure
|
||||
each individual filter is
|
||||
|
||||
It also checks that EDIT scope security is not being applied to tables
|
||||
with a REPLACE loadtype (these are incompatible - see issue #211).
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li dc_assignlib.sas
|
||||
@li mp_abort.sas
|
||||
@li mp_filtercheck.sas
|
||||
|
||||
<h4> Related Macros </h4>
|
||||
@@ -23,6 +27,33 @@
|
||||
**/
|
||||
|
||||
|
||||
/* REPLACE loadtype is incompatible with row level EDIT security (#211) */
|
||||
proc sql noprint;
|
||||
create table work.badloadtypes as
|
||||
select distinct cats(upcase(a.rls_libref),'.',upcase(a.rls_table)) as libds
|
||||
from work.staging_ds(where=(rls_active=1
|
||||
and upcase(rls_scope) in ('EDIT','ALL')
|
||||
and upcase(_____DELETE__THIS__RECORD_____) ne 'YES')) a
|
||||
inner join &dc_libref..mpe_tables(where=(&dc_dttmtfmt. lt tx_to)) b
|
||||
on upcase(a.rls_libref)=upcase(b.libref)
|
||||
and upcase(a.rls_table)=upcase(b.dsn)
|
||||
where upcase(b.loadtype)='REPLACE';
|
||||
|
||||
%let badloadtypes=0;
|
||||
%let badloadtype=;
|
||||
data _null_;
|
||||
set work.badloadtypes;
|
||||
call symputx('badloadtypes',_n_);
|
||||
call symputx('badloadtype',libds);
|
||||
run;
|
||||
|
||||
%mp_abort(iftrue=(&badloadtypes ne 0)
|
||||
,mac=mpe_row_level_security_postedit
|
||||
,msg=%str(Row level EDIT security cannot be applied to table(s) with a%trim(
|
||||
) REPLACE loadtype, eg: &badloadtype. Remove the security rule(s) or %trim(
|
||||
)change the loadtype.)
|
||||
)
|
||||
|
||||
/* ignore scope and group for validation */
|
||||
proc sql;
|
||||
create table work.batches as
|
||||
|
||||
@@ -69,6 +69,33 @@ run;
|
||||
,msg=%superq(errmsg)
|
||||
)
|
||||
|
||||
/* REPLACE loadtype is incompatible with row level EDIT security (#211) */
|
||||
proc sql noprint;
|
||||
create table work.badloadtypes as
|
||||
select distinct cats(upcase(a.libref),'.',upcase(a.dsn)) as libds
|
||||
from work.staging_ds(where=(loadtype='REPLACE'
|
||||
and upcase(_____DELETE__THIS__RECORD_____) ne 'YES')) a
|
||||
inner join &dc_libref..mpe_row_level_security(where=(
|
||||
rls_active=1 and &dc_dttmtfmt. lt tx_to
|
||||
and upcase(rls_scope) in ('EDIT','ALL'))) b
|
||||
on upcase(a.libref)=upcase(b.rls_libref)
|
||||
and upcase(a.dsn)=upcase(b.rls_table);
|
||||
|
||||
%let badloadtypes=0;
|
||||
%let badloadtype=;
|
||||
data _null_;
|
||||
set work.badloadtypes;
|
||||
call symputx('badloadtypes',_n_);
|
||||
call symputx('badloadtype',libds);
|
||||
run;
|
||||
|
||||
%mp_abort(iftrue=(&badloadtypes ne 0)
|
||||
,mac=mpe_tables_postedit
|
||||
,msg=%str(REPLACE loadtype cannot be applied to table(s) with row level %trim(
|
||||
)EDIT security, eg: &badloadtype. Remove the security rule(s) or change%trim(
|
||||
) the loadtype.)
|
||||
)
|
||||
|
||||
/* get distinct list of audit libs */
|
||||
proc sql;
|
||||
create table work.liblist as
|
||||
|
||||
Reference in New Issue
Block a user