diff --git a/DESCRIPTION b/DESCRIPTION index 263960c..3fc07d9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: dsROCrate Title: 'DataSHIELD' RO-Crate Governance Functions -Version: 0.2.1 +Version: 0.2.2 Authors@R: c( person(given = "Roberto", family = "Villegas-Diaz", diff --git a/NEWS.md b/NEWS.md index 8e41bee..ee3e9ee 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,12 @@ +# dsROCrate 0.2.2 + +## Bug fixes + +* Fixed an issue in `safe_output()` where Opal audit logs containing no `AGGREGATE` events could cause the audit pipeline to fail when constructing the calls table. +* `safe_output()` now handles audit sessions with logs but no `AGGREGATE` calls gracefully, returning an empty calls table rather than attempting to operate on `NULL`. +* Fixed an issue where an RO-Crate without an explicit Opal profile attribute could cause safe_output() to fail when filtering audit logs. The default "default" Opal profile is now used when no profile is supplied. +* Improved robustness of provenance extraction for audit sessions with incomplete or limited audit activity. + # dsROCrate 0.2.1 ## Bug Fixes diff --git a/R/audit.R b/R/audit.R index 6aa1852..a7216e3 100644 --- a/R/audit.R +++ b/R/audit.R @@ -29,8 +29,15 @@ #' @param ... Additional arguments. #' @param intent Additional object with governance bundle/specification of the #' intent of a project. It takes the same types as `x`. -#' @param project String with project name(s) from which to extra Safe Project +#' @param profile String with profile name (used for OBiBa's Opal backend). +#' @param project String with project name(s) from which to extract Safe Project #' details. +#' @param resources Vector of strings with the names of the resources, part of +#' `project`. Optional, if not provided, all the resources associated to +#' `project` will be included in the RO-Crate. +#' @param tables Vector of strings with the names of the tables/datasets, part +#' of `project`. Optional, if not provided, all the tables/datasets +#' associated to `project` will be included in the RO-Crate. #' @param user String with the user name for which to extract Safe People #' details. #' @param logs_from Lower limit timestamp to filter out the outputs generated @@ -142,7 +149,10 @@ audit.opal <- function( x, ..., intent = NULL, + profile = "default", project = NULL, + resources = NULL, + tables = NULL, user = NULL, logs_from = -Inf, logs_to = Inf, @@ -154,7 +164,10 @@ audit.opal <- function( # call next method main_audit <- audit_engine( x, + profile = profile, project = c(intent_lst$main_audit_args$project, project), + resources = resources, + tables = tables, user = c(intent_lst$main_audit_args$user, user), logs_from = logs_from, logs_to = logs_to, diff --git a/R/audit_engine.R b/R/audit_engine.R index 000cf52..78c1e35 100644 --- a/R/audit_engine.R +++ b/R/audit_engine.R @@ -7,8 +7,15 @@ #' governance archive file, representing the intent of a project and #' associated governance details. #' @param ... Other optional arguments, see full documentation for details. -#' @param project String with project name(s) from which to extra Safe Project +#' @param profile String with profile name (used for OBiBa's Opal backend). +#' @param project String with project name(s) from which to extract Safe Project #' details. +#' @param resources Vector of strings with the names of the resources, part of +#' `project`. Optional, if not provided, all the resources associated to +#' `project` will be included in the RO-Crate. +#' @param tables Vector of strings with the names of the tables/datasets, part +#' of `project`. Optional, if not provided, all the tables/datasets +#' associated to `project` will be included in the RO-Crate. #' @param user String with the user name for which to extract Safe People #' details. #' @param logs_from Lower limit timestamp to filter out the outputs generated @@ -61,7 +68,10 @@ audit_engine.cr8tor <- function(x, ...) { audit_engine.opal <- function( x, ..., + profile = "default", project = NULL, + resources = NULL, + tables = NULL, user = NULL, logs_from = -Inf, logs_to = Inf, @@ -70,11 +80,16 @@ audit_engine.opal <- function( # local bindings name <- NULL - # create RO-Create with the 5 safes profile - crate <- rocrateR::rocrate_5s() - - # validate backend - validate_backend(x, ...) + # initialise empty RO-Crate with audit settings + crate <- x |> + dsROCrate::init( + profile = profile, + project = project, + resources = resources, + tables = tables, + user = user, + path = path + ) # if `project` is missing, then error if (is.null(project)) { @@ -165,7 +180,7 @@ audit_engine.opal <- function( purrr::reduce(rocrateR::remove_entity, .init = crate) # Safe Settings ---- - crate <- safe_setting(x, rocrate = crate) + crate <- safe_setting(crate, connection = x) # Safe Outputs ---- crate <- safe_people_tbl$name |> diff --git a/R/backend-opal.R b/R/backend-opal.R index 1d98694..c8dec8c 100644 --- a/R/backend-opal.R +++ b/R/backend-opal.R @@ -9,8 +9,8 @@ backend_options.opal <- function(x, ...) { } #' @export -backend_packages.opal <- function(x, ...) { - opalr::dsadmin.package_descriptions(x, ...) +backend_packages.opal <- function(x, ..., profile = "default") { + opalr::dsadmin.package_descriptions(x, profile = profile, ...) } #' @export diff --git a/R/safe_output.R b/R/safe_output.R index 42055f8..cb183b8 100644 --- a/R/safe_output.R +++ b/R/safe_output.R @@ -104,7 +104,11 @@ safe_output.opal <- function( `@timestamp` <- backend <- logger_name <- safe_people_id <- username <- NULL ds_action <- ds_eval <- ds_id <- ds_function <- ds_symbol <- ds_table <- NULL asset <- action <- is_placeholder <- kind <- symbol_id <- timestamp <- NULL - expr <- fx <- log_id <- r_cmd <- session <- symbol <- NULL + ds_profile <- expr <- fx <- log_id <- r_cmd <- session <- symbol <- NULL + + if (is.null(profile)) { + profile <- "default" + } # create formatted versions of input dates logs_from_is_valid <- FALSE @@ -217,12 +221,14 @@ safe_output.opal <- function( # parse logs userlogs_tbl <- backend_logs(x) |> tibble::as_tibble() |> + dplyr::bind_rows(tibble::tibble(ds_profile = "-999999")) |> dplyr::mutate( `@timestamp` = as.POSIXct(`@timestamp`, format = "%Y-%m-%dT%H:%M:%S") ) |> # filter logs dplyr::filter(`@timestamp` >= logs_from, `@timestamp` <= logs_to) |> dplyr::filter(logger_name == "datashield.user") |> + dplyr::filter(ds_profile == profile) |> dplyr::filter(username %in% user) userlogs <- NULL @@ -350,27 +356,7 @@ safe_output.opal <- function( }) # convert list of calls into tibble - calls_tbl <- purrr::map(calls_lst, \(x) { - tibble::tibble( - timestamp = format(x$created_at, '%Y-%m-%dT%H:%M:%S'), - action = "AGGREGATE", - user = x$user, - r_cmd = x$original, - fx = paste0(x$package, x$namespace, x$fx), - args = list(x$args), - symbol = NA, - table = x$args |> - purrr::map(function(x) { - if (!inherits(x, "safe_reference")) { - return(NA_character_) - } - resolve_symbol_asset(x$symbol_id, registry) - }), - session = x$session, - profile = x$profile - ) - }) |> - purrr::list_c() + calls_tbl <- calls_to_tbl(calls_lst, registry) # combine function calls with symbol's registry calls_symbols_tbl <- calls_tbl |> diff --git a/R/safe_setting.R b/R/safe_setting.R index c13638c..8d90013 100644 --- a/R/safe_setting.R +++ b/R/safe_setting.R @@ -200,7 +200,7 @@ safe_setting.opal <- function( # computational environment ---- # extract information about R packages installed in the environment - pkg_tbl <- backend_packages(x) |> + pkg_tbl <- backend_packages(x, profile = profile) |> tibble::as_tibble() pkg_entities <- pkg_tbl |> purrr::pmap(function(Package, Version, Description, Author, ...) { diff --git a/R/utils-safe_output.R b/R/utils-safe_output.R index e637a9d..78f3578 100644 --- a/R/utils-safe_output.R +++ b/R/utils-safe_output.R @@ -1,3 +1,53 @@ +#' Convert log calls into tibble +#' +#' @param calls List with log calls. +#' @param registry Symbol registry. +#' +#' @returns Tibble with parsed log calls. +#' @keywords internal +#' +#' @noRd +calls_to_tbl <- function(calls, registry) { + if (length(calls) == 0L) { + return( + tibble::tibble( + timestamp = character(), + action = character(), + user = character(), + r_cmd = character(), + fx = character(), + args = list(), + symbol = character(), + table = character(), + session = character(), + profile = character() + ) + ) + } + + purrr::map(calls, \(x) { + tibble::tibble( + timestamp = format(x$created_at, "%Y-%m-%dT%H:%M:%S"), + action = "AGGREGATE", + user = x$user, + r_cmd = x$original, + fx = paste0(x$package, x$namespace, x$fx), + args = list(x$args), + symbol = NA_character_, + table = x$args |> + purrr::map(function(x) { + if (!inherits(x, "safe_reference")) { + return(NA_character_) + } + resolve_symbol_asset(x$symbol_id, registry) + }), + session = x$session, + profile = x$profile + ) + }) |> + purrr::list_rbind() +} + #' Extract Safe Output entity(ies) #' #' @inheritParams safe_data diff --git a/man/audit.Rd b/man/audit.Rd index 6d1e009..83fdebd 100644 --- a/man/audit.Rd +++ b/man/audit.Rd @@ -24,7 +24,10 @@ audit(x, ...) x, ..., intent = NULL, + profile = "default", project = NULL, + resources = NULL, + tables = NULL, user = NULL, logs_from = -Inf, logs_to = Inf, @@ -48,9 +51,19 @@ Alternatively, a list of any of the above.} \item{intent}{Additional object with governance bundle/specification of the intent of a project. It takes the same types as \code{x}.} -\item{project}{String with project name(s) from which to extra Safe Project +\item{profile}{String with profile name (used for OBiBa's Opal backend).} + +\item{project}{String with project name(s) from which to extract Safe Project details.} +\item{resources}{Vector of strings with the names of the resources, part of +\code{project}. Optional, if not provided, all the resources associated to +\code{project} will be included in the RO-Crate.} + +\item{tables}{Vector of strings with the names of the tables/datasets, part +of \code{project}. Optional, if not provided, all the tables/datasets +associated to \code{project} will be included in the RO-Crate.} + \item{user}{String with the user name for which to extract Safe People details.}