From 0673c25ea0c1d27357b0777390970e08b8f92678 Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Wed, 11 Dec 2024 09:40:24 -0800 Subject: [PATCH] fix #15 add restrictive fxn for as part of RLS policy; update vignette --- NAMESPACE | 1 + R/as_row_policy.R | 2 ++ R/row_policy.R | 71 ++++++++++++++++++++++++++++++------- _pkgdown.yml | 1 + man/commands.Rd | 4 +++ man/restrictive.Rd | 33 +++++++++++++++++ man/row_policy.Rd | 19 ++++++++++ man/rows_existing.Rd | 4 +++ man/rows_new.Rd | 4 +++ man/translate_row_policy.Rd | 7 +++- vignettes/rls.Rmd | 3 ++ vignettes/rls.Rmd.og | 6 +++- 12 files changed, 141 insertions(+), 14 deletions(-) create mode 100644 man/restrictive.Rd diff --git a/NAMESPACE b/NAMESPACE index 5532d03..b17c0c3 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -17,6 +17,7 @@ export(commands) export(from) export(grant) export(has_postgres) +export(restrictive) export(revoke) export(rls_check_status) export(rls_col_policy) diff --git a/R/as_row_policy.R b/R/as_row_policy.R index e80b487..c45bed8 100644 --- a/R/as_row_policy.R +++ b/R/as_row_policy.R @@ -14,6 +14,7 @@ as_row_policy.tbl_sql <- function(x) { tmp <- list( data = x, name = NULL, + as = NULL, commands = NULL, user = NULL, existing_rows = NULL, @@ -25,6 +26,7 @@ as_row_policy.tbl_sql <- function(x) { #' @export print.row_policy <- function(x, ...) { cat_line(glue(" {x$name}")) + cat_me("as", x$as %||% "PERMISSIVE") if (!is_really_empty(x$user)) { cat_me("user", x$user) } diff --git a/R/row_policy.R b/R/row_policy.R index 4bb6252..c945fc4 100644 --- a/R/row_policy.R +++ b/R/row_policy.R @@ -1,8 +1,22 @@ #' Row policy -#' +#' #' @export #' @inheritParams grant #' @param name (character) scalar name for the policy. required +#' @return an S3 class `row_policy`; see [row_policy()] for its +#' structure +#' @details The return object and all functions that build on this +#' function return an S3 class called `row_policy` which is just +#' a named list with slots: +#' +#' - data +#' - name +#' - as +#' - commands +#' - user +#' - existing_rows +#' - new_rows +#' - type #' @examplesIf has_postgres() #' library(DBI) #' library(RPostgres) @@ -14,7 +28,7 @@ #' row_policy("my_policy") %>% #' rls_run() #' rls_policies(con) -#' +#' #' # cleanup #' rls_drop_policies(con) #' dbDisconnect(con) @@ -28,9 +42,10 @@ row_policy <- function(.data, name) { } #' Commands -#' +#' #' @export #' @inheritParams grant +#' @inherit row_policy return #' @examplesIf has_postgres() #' library(DBI) #' library(RPostgres) @@ -41,7 +56,7 @@ row_policy <- function(.data, name) { #' rls_tbl(con, "passwd") %>% #' row_policy("their_policy") %>% #' commands(update) -#' +#' #' # cleanup #' dbDisconnect(con) commands <- function(.data, ...) { @@ -52,9 +67,10 @@ commands <- function(.data, ...) { } #' Create rule for existing rows -#' +#' #' @export #' @inheritParams grant +#' @inherit row_policy return #' @param using an expression to use to check against existing rows #' @param sql (character) sql syntax to use for existing rows #' @details Use either `using` or `sql`, not both @@ -70,7 +86,7 @@ commands <- function(.data, ...) { #' row_policy("a_good_policy") %>% #' commands(update) %>% #' rows_existing(sql = 'current_user = "user_name"') -#' +#' #' # cleanup #' dbDisconnect(con) rows_existing <- function(.data, using = NULL, sql = NULL) { @@ -88,10 +104,11 @@ rows_existing <- function(.data, using = NULL, sql = NULL) { } #' Create rule for new rows -#' +#' #' @export #' @importFrom dbplyr translate_sql #' @inheritParams grant +#' @inherit row_policy return #' @param check an expression to use to check against addition of #' new rows or editing of existing rows #' @param sql (character) sql syntax to use for new rows @@ -118,7 +135,7 @@ rows_existing <- function(.data, using = NULL, sql = NULL) { #' rows_existing(sql = 'current_user = "user_name"') %>% #' rows_new(home_phone == "098-765-4321") %>% #' to(jane) -#' +#' #' # cleanup #' dbDisconnect(con) rows_new <- function(.data, check = NULL, sql = NULL) { @@ -146,6 +163,32 @@ express <- function(x) { glue("({ifelse(x == 'TRUE', tolower(x), x)})") } +#' Set RLS policy to be restrictive +#' +#' @export +#' @inherit row_policy return +#' @details By default row level policies are permissive. Permissive policies +#' are applied using a boolean "OR", so you need permission from only one +#' policy to be able to query a certain row. Whereas for restrictive policies, +#' they are applied using a boolean "AND" so you have to pass all restrictive +#' policies for each row you want to query. +#' @examples +#' library(DBI) +#' library(RPostgres) +#' con <- dbConnect(Postgres()) +#' if (!dbExistsTable(con, "passwd")) { +#' setup_example_table(con, "passwd") +#' } +#' +#' rls_tbl(con, "passwd") %>% row_policy("their_policy") +#' rls_tbl(con, "passwd") %>% row_policy("their_policy") %>% restrictive() +restrictive <- function(.data) { + pipe_autoexec(toggle = rls_env$auto_pipe) + .data <- as_row_policy(.data) + .data$as <- "RESTRICTIVE" + .data +} + #' Translate row policy #' #' @export @@ -153,12 +196,13 @@ express <- function(x) { #' @param policy an S3 object of class `row_policy`, required #' @param con DBI connection object, required #' @references +#' @return an S3 class [dbplyr::sql()] #' @examplesIf has_postgres() #' library(DBI) #' library(RPostgres) #' con <- dbConnect(Postgres()) #' setup_example_table(con) -#' +#' #' # create role #' dbExecute(con, "CREATE ROLE jane") #' @@ -166,7 +210,9 @@ express <- function(x) { #' rls_drop_policy(con, name = "blue_policy", table = "passwd") #' } #' -#' policy <- rls_tbl(con, "passwd") %>% +#' policy <- +#' rls_tbl(con, "passwd") %>% +#' restrictive() %>% #' row_policy(name = "blue_policy") %>% #' commands(update) %>% #' rows_existing(TRUE) %>% @@ -176,7 +222,7 @@ express <- function(x) { #' sql <- translate_row_policy(policy, con) #' sql #' dbExecute(con, sql) -#' +#' #' # cleanup #' rls_drop_policies(con) #' dbExecute(con, "DROP ROLE jane") @@ -189,10 +235,11 @@ translate_row_policy <- function(policy, con) { ) sql_create_policy <- glue(" {create_statement} POLICY {policy$name} ON {attr(policy$data, 'table')} + {combine_if('AS', policy$as %||% 'PERMISSIVE')} {combine_if('FOR', policy$commands)} {combine_if('TO', policy$user)} {combine_if('USING', policy$existing_rows, express)} {combine_if('WITH CHECK', policy$new_rows, express)} ") - sql(gsub("\n\\s+\n", "\n", sql_create_policy)) + sql(sub("^\\s+", "", gsub("\n\\s+\n", "\n", sql_create_policy))) } diff --git a/_pkgdown.yml b/_pkgdown.yml index 66def38..c2e8e05 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -25,6 +25,7 @@ reference: - row_policy - rows_existing - rows_new + - restrictive - title: Privileges (aka column level security) contents: - as_priv diff --git a/man/commands.Rd b/man/commands.Rd index 03a753c..9c43185 100644 --- a/man/commands.Rd +++ b/man/commands.Rd @@ -11,6 +11,10 @@ commands(.data, ...) \item{...}{one of all, select, update, insert, delete} } +\value{ +an S3 class \code{row_policy}; see \code{\link[=row_policy]{row_policy()}} for its +structure +} \description{ Commands } diff --git a/man/restrictive.Rd b/man/restrictive.Rd new file mode 100644 index 0000000..940a78d --- /dev/null +++ b/man/restrictive.Rd @@ -0,0 +1,33 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/row_policy.R +\name{restrictive} +\alias{restrictive} +\title{Set RLS policy to be restrictive} +\usage{ +restrictive(.data) +} +\value{ +an S3 class \code{row_policy}; see \code{\link[=row_policy]{row_policy()}} for its +structure +} +\description{ +Set RLS policy to be restrictive +} +\details{ +By default row level policies are permissive. Permissive policies +are applied using a boolean "OR", so you need permission from only one +policy to be able to query a certain row. Whereas for restrictive policies, +they are applied using a boolean "AND" so you have to pass all restrictive +policies for each row you want to query. +} +\examples{ +library(DBI) +library(RPostgres) +con <- dbConnect(Postgres()) +if (!dbExistsTable(con, "passwd")) { + setup_example_table(con, "passwd") +} + +rls_tbl(con, "passwd") \%>\% row_policy("their_policy") +rls_tbl(con, "passwd") \%>\% row_policy("their_policy") \%>\% restrictive() +} diff --git a/man/row_policy.Rd b/man/row_policy.Rd index cccec42..7087e1e 100644 --- a/man/row_policy.Rd +++ b/man/row_policy.Rd @@ -11,9 +11,28 @@ row_policy(.data, name) \item{name}{(character) scalar name for the policy. required} } +\value{ +an S3 class \code{row_policy}; see \code{\link[=row_policy]{row_policy()}} for its +structure +} \description{ Row policy } +\details{ +The return object and all functions that build on this +function return an S3 class called \code{row_policy} which is just +a named list with slots: +\itemize{ +\item data +\item name +\item as +\item commands +\item user +\item existing_rows +\item new_rows +\item type +} +} \examples{ \dontshow{if (has_postgres()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} library(DBI) diff --git a/man/rows_existing.Rd b/man/rows_existing.Rd index 5fffade..d1d06a7 100644 --- a/man/rows_existing.Rd +++ b/man/rows_existing.Rd @@ -13,6 +13,10 @@ rows_existing(.data, using = NULL, sql = NULL) \item{sql}{(character) sql syntax to use for existing rows} } +\value{ +an S3 class \code{row_policy}; see \code{\link[=row_policy]{row_policy()}} for its +structure +} \description{ Create rule for existing rows } diff --git a/man/rows_new.Rd b/man/rows_new.Rd index b3a47ab..b4a62eb 100644 --- a/man/rows_new.Rd +++ b/man/rows_new.Rd @@ -14,6 +14,10 @@ new rows or editing of existing rows} \item{sql}{(character) sql syntax to use for new rows} } +\value{ +an S3 class \code{row_policy}; see \code{\link[=row_policy]{row_policy()}} for its +structure +} \description{ Create rule for new rows } diff --git a/man/translate_row_policy.Rd b/man/translate_row_policy.Rd index b010d9b..68795b4 100644 --- a/man/translate_row_policy.Rd +++ b/man/translate_row_policy.Rd @@ -11,6 +11,9 @@ translate_row_policy(policy, con) \item{con}{DBI connection object, required} } +\value{ +an S3 class \code{\link[dbplyr:sql]{dbplyr::sql()}} +} \description{ Translate row policy } @@ -28,7 +31,9 @@ if (rls_policy_exists(con, "blue_policy")) { rls_drop_policy(con, name = "blue_policy", table = "passwd") } -policy <- rls_tbl(con, "passwd") \%>\% +policy <- +rls_tbl(con, "passwd") \%>\% + restrictive() \%>\% row_policy(name = "blue_policy") \%>\% commands(update) \%>\% rows_existing(TRUE) \%>\% diff --git a/vignettes/rls.Rmd b/vignettes/rls.Rmd index 2c9c1c0..937e3d1 100644 --- a/vignettes/rls.Rmd +++ b/vignettes/rls.Rmd @@ -138,6 +138,7 @@ policy1 <- rls_tbl(con, "passwd") %>% to(admin) policy1 #> admin_all +#> as: PERMISSIVE #> user: admin #> existing rows: TRUE #> new rows: TRUE @@ -167,6 +168,7 @@ policy2 <- rls_tbl(con, "passwd") %>% rows_existing(TRUE) policy2 #> all_view +#> as: PERMISSIVE #> commands: select #> existing rows: TRUE #> # Source: SQL [3 x 8] @@ -201,6 +203,7 @@ policy3 <- rls_tbl(con, "passwd") %>% ) policy3 #> user_mod +#> as: PERMISSIVE #> commands: update #> existing rows: current_user = user_name #> new rows: diff --git a/vignettes/rls.Rmd.og b/vignettes/rls.Rmd.og index a7e1e69..b7708e0 100644 --- a/vignettes/rls.Rmd.og +++ b/vignettes/rls.Rmd.og @@ -38,7 +38,12 @@ con <- dbConnect(Postgres()) ``` ```{r echo=FALSE} +# get current user user <- dbGetQuery(con, "select current_user")$current_user +# delete passwd table if it exists +if (dbExistsTable(con, "passwd")) { + dbRemoveTable(con, "passwd") +} ``` ## Create roles @@ -268,4 +273,3 @@ dbExecute(con, "DROP ROLE admin") dbExecute(con, "DROP ROLE bob") dbExecute(con, "DROP ROLE alice") ``` -