diff --git a/.Rbuildignore b/.Rbuildignore index 7e686a9..cf0abdd 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -11,3 +11,4 @@ ^\.github$ (^|/)\.DS\_Store$ ^\.git($|/) +^CRAN-SUBMISSION$ diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml new file mode 100644 index 0000000..3baf405 --- /dev/null +++ b/.github/workflows/R-CMD-check.yaml @@ -0,0 +1,53 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + push: + branches: [main, master] + pull_request: + +name: R-CMD-check.yaml + +permissions: read-all + +jobs: + R-CMD-check: + runs-on: ${{ matrix.config.os }} + + name: ${{ matrix.config.os }} (${{ matrix.config.r }}) + + strategy: + fail-fast: false + matrix: + config: + - {os: macos-latest, r: 'release'} + - {os: windows-latest, r: 'release'} + - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} + - {os: ubuntu-latest, r: 'release'} + - {os: ubuntu-latest, r: 'oldrel-1'} + + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + R_KEEP_PKG_SOURCE: yes + + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: ${{ matrix.config.r }} + http-user-agent: ${{ matrix.config.http-user-agent }} + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: | + any::rcmdcheck + local::. + needs: check + + - uses: r-lib/actions/check-r-package@v2 + with: + upload-snapshots: true + build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index bfc9f4d..1d05bbf 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -33,7 +33,9 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - extra-packages: any::pkgdown, local::. + extra-packages: | + any::pkgdown + local::. needs: website - name: Build site diff --git a/.github/workflows/test-coverage.yaml b/.github/workflows/test-coverage.yaml index 0ab748d..40b3dd7 100644 --- a/.github/workflows/test-coverage.yaml +++ b/.github/workflows/test-coverage.yaml @@ -24,7 +24,10 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - extra-packages: any::covr, any::xml2 + extra-packages: | + any::covr + any::xml2 + local::. needs: coverage - name: Test coverage @@ -34,7 +37,6 @@ jobs: clean = FALSE, install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package") ) - print(cov) covr::to_cobertura(cov) shell: Rscript {0} diff --git a/.gitignore b/.gitignore index c7306ca..2b827fe 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ rsconnect/ inst/doc **/.quarto/ inst/R.ignore +cran-comments.md +CRAN-SUBMISSION diff --git a/DESCRIPTION b/DESCRIPTION index d660749..f8707ec 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,5 +1,5 @@ Package: dsROCrate -Title: 'DataSHIELD' RO-Crate Wrapper Functions +Title: 'DataSHIELD' RO-Crate Governance Functions Version: 0.0.1 Authors@R: c( person(given = "Roberto", @@ -7,7 +7,7 @@ Authors@R: c( role = c("aut", "cre"), email = "r.villegas-diaz@outlook.com", comment = c(ORCID = "0000-0001-5036-8661")), - person(given = "Rebecca", + person(given = "Becca", family = "Wilson", role = c("aut"), comment = c(ORCID = "0000-0003-2294-593X")), @@ -20,17 +20,19 @@ Authors@R: c( role = c("aut"), comment = c(ORCID = "0009-0003-2419-1964")), person("University of Liverpool", role = c("cph"))) -Description: R package to wrap elements from 'DataSHIELD' analysis into an - RO-Crate. +Description: Tools for wrapping 'DataSHIELD' analyses into RO-Crate + (Research Object Crate) objects. Provides functions to create structured + metadata for federated data analysis projects, enabling governance + tracking of data access, project membership, analysis execution and + output validation across distributed data sources. License: MIT + file LICENSE Suggests: dsBaseClient, DSI, - DSMolgenisArmadillo, DSOpal, + fs, knitr, MolgenisArmadillo, - opalr, testthat (>= 3.0.0), withr Config/testthat/edition: 3 @@ -41,12 +43,13 @@ RoxygenNote: 7.3.3 Imports: digest, dplyr, + DSMolgenisArmadillo, jsonlite, - methods, + opalr, purrr, RcppTOML, rmarkdown, - rocrateR (>= 0.0.1), + rocrateR (>= 0.1.0), tibble, vtree, xptr, @@ -54,32 +57,5 @@ Imports: Depends: R (>= 4.1.0) VignetteBuilder: knitr -Collate: - 'ArmadilloCredentials-class.R' - 'audit_cr8tor.R' - 'audit_safe_people.R' - 'audit_safe_project.R' - 'audit_study.R' - 'dsROCrate-package.R' - 'dsROCrate.R' - 'rocrate_report.R' - 'safe_data.R' - 'safe_output.R' - 'safe_people.R' - 'safe_project.R' - 'safe_setting.R' - 'utils-armadillo.R' - 'utils-connection.R' - 'utils-cr8tor.R' - 'utils-date.R' - 'utils-digest.R' - 'utils-opal.R' - 'utils-rocrate.R' - 'utils-rocrateR.R' - 'utils-safe_data.R' - 'utils-safe_output.R' - 'utils-safe_people.R' - 'utils-safe_project.R' - 'utils-safe_setting.R' - 'utils-show.R' - 'utils-tibble.R' +URL: https://github.com/FederatedMethods/dsROCrate +BugReports: https://github.com/FederatedMethods/dsROCrate/issues diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d8f6df1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,2 @@ +YEAR: 2026 +COPYRIGHT HOLDER: University of Liverpool diff --git a/LICENSE.md b/LICENSE.md index 2d18375..632fa02 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ # MIT License -Copyright (c) 2025 dsROCrate authors +Copyright (c) 2026 University of Liverpool Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/NAMESPACE b/NAMESPACE index 59f7d23..b8a79c3 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,12 +1,13 @@ # Generated by roxygen2: do not edit by hand -S3method(audit_safe_people,default) -S3method(audit_safe_people,opal) -S3method(audit_safe_project,default) -S3method(audit_safe_project,opal) -S3method(audit_study,default) -S3method(audit_study,list) -S3method(audit_study,opal) +S3method(audit,ArmadilloCredentials) +S3method(audit,character) +S3method(audit,cr8tor) +S3method(audit,list) +S3method(audit,opal) +S3method(audit,rocrate) +S3method(audit_engine,cr8tor) +S3method(audit_engine,opal) S3method(extract_safe_data,opal) S3method(extract_safe_data,rocrate) S3method(extract_safe_output,opal) @@ -29,10 +30,13 @@ S3method(flatten_safe_setting,default) S3method(flatten_safe_setting,rocrate) S3method(init,opal) S3method(init,rocrate) -S3method(rocrate_report,character) -S3method(rocrate_report,default) -S3method(rocrate_report,list) -S3method(rocrate_report,rocrate) +S3method(parse_user_profiles,ArmadilloCredentials) +S3method(print,cr8tor_bundle) +S3method(project_exists,ArmadilloCredentials) +S3method(report,character) +S3method(report,default) +S3method(report,list) +S3method(report,rocrate) S3method(safe_data,character) S3method(safe_data,default) S3method(safe_data,opal) @@ -45,6 +49,7 @@ S3method(safe_people,character) S3method(safe_people,default) S3method(safe_people,opal) S3method(safe_people,rocrate) +S3method(safe_project,ArmadilloCredentials) S3method(safe_project,character) S3method(safe_project,default) S3method(safe_project,opal) @@ -55,19 +60,13 @@ S3method(safe_setting,default) S3method(safe_setting,opal) S3method(safe_setting,rocrate) export(armadillo_login) -export(audit_cr8tor) -export(audit_safe_people) -export(audit_safe_project) -export(audit_study) +export(audit) export(init) -export(load_cr8tor_bundle) -export(rocrate_report) +export(report) export(safe_data) export(safe_output) export(safe_people) export(safe_project) export(safe_setting) -exportMethods(safe_project) -import(methods) importFrom(rocrateR,get_entity) importFrom(utils,write.csv) diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..56ef97b --- /dev/null +++ b/NEWS.md @@ -0,0 +1,6 @@ +# dsROCrate 0.0.1 + +* Initial CRAN submission. +* This version contains standard functions for auditing (`audit()`), reporting +(`report()`) and extracting five safe principle components (`safe_*()`). +* This version currently only supports OBiBa's Opal as the backend. diff --git a/R/ArmadilloCredentials-class.R b/R/ArmadilloCredentials-class.R deleted file mode 100644 index ec49c37..0000000 --- a/R/ArmadilloCredentials-class.R +++ /dev/null @@ -1,17 +0,0 @@ -# define virtual S4 class `armadillo` -setClass("armadillo", contains = "VIRTUAL") - -# define S4 class `ArmadilloCredentials` -setClass( - "ArmadilloCredentials", - contains = "armadillo", - slots = list( - access_token = "character", - id_token = "character", - refresh_token = "character", - token_type = "character", - auth_type = "character", - expires_in = "integer", - expires_at = "POSIXct" - ) -) diff --git a/R/audit.R b/R/audit.R new file mode 100644 index 0000000..69ecea9 --- /dev/null +++ b/R/audit.R @@ -0,0 +1,150 @@ +#' Create an audit RO-Crate +#' +#' Create an audit RO-Crate following the 5 Safes Principles. +#' +#' This function handles various audit types, which will be dispatched based on +#' the input object. If the input object is +#' +#' \itemize{ +#' \item a _connection_ to a DataSHIELD server (e.g., OBiBa's Opal): +#'. generates an RO-Crate object with deployment details, including outputs. +#' \item a _path_ pointing to +#' \itemize{ +#' \item **a `cr8tor` archive / governance bundle**: generates an +#' RO-Crate object with pre-deployment governance details. +#' \item **an RO-Crate object**: generates an RO-Crate object with +#' clearly defined 5 Safes elements. +#' } +#' \item an _RO-Crate_ object: generates an RO-Crate object with clearly +#' defined 5 Safes elements. +#' } +#' @param x Object to be audited. This can be +#' \itemize{ +#' \item a _connection_ to a DataSHIELD server (e.g., OBiBa's Opal). +#' \item a _path_ pointing to an RO-Crate OR a `cr8tor` archive / +#'. governance bundle. +#' \item an _RO-Crate_ object. +#' } +#' Alternatively, a list of any of the above. +#' @param ... Additional arguments. +#' @param intent Additional object with governance bundle/specification of the +#' intent of a project. It takes the same types as `x`. +#' @inheritParams audit_engine +#' +#' @returns RO-Crate with audit details. +#' @export +audit <- function(x, ...) { + UseMethod("audit") +} + +#' @rdname audit +#' @export +audit.ArmadilloCredentials <- function(x, ..., intent = NULL) { + stop( + "The audit for Armadillo backend is not currently implemented!", + call. = FALSE + ) +} + +#' @rdname audit +#' @export +audit.character <- function(x, ..., intent = NULL) { + # verify if the given path exists, if not, return an error message + if (!file.exists(x)) { + stop("The given file does not exist!", call. = FALSE) + } + + # attempt loading a `cr8tor` bundle + x_obj <- tryCatch( + load_cr8tor_bundle(x, ...), + error = function(e) NULL + ) + # alternatively, attempt loading an RO-Crate + if (is.null(x_obj)) { + x_obj <- tryCatch( + rocrateR::load_rocrate(x, ...), + error = function(e) NULL + ) + } + + if (is.null(x_obj)) { + stop( + "The given path does not point to a valid `cr8tor` archive nor an `rocrate", + call. = FALSE + ) + } + + # attempt auditing intent + intent_lst <- audit_intent(intent, ...) + + # call next method + main_audit <- audit(x_obj, intent_lst$main_audit_args) + + # return list + if (is.null(intent_lst$intent_audit)) { + return(main_audit) + } + list(intent = intent_lst$intent_audit, deployment = main_audit) +} + +#' @rdname audit +#' @export +audit.cr8tor <- function(x, ..., intent = NULL) { + # attempt auditing intent + intent_lst <- audit_intent(intent, ..., excluded_args = "path") + + # call next method + main_audit <- audit_engine(x, intent_lst$main_audit_args) + + # return list + if (is.null(intent_lst$intent_audit)) { + return(main_audit) + } + list(intent = intent_lst$intent_audit, deployment = main_audit) +} + +#' @rdname audit +#' @export +audit.list <- function(x, ..., intent = NULL) { + purrr::map(x, audit, ..., intent = intent) +} + +#' @rdname audit +#' @export +audit.opal <- function( + x, + ..., + intent = NULL, + project = NULL, + user = NULL, + logs_from = -Inf, + logs_to = Inf, + path = NULL +) { + # attempt auditing intent + intent_lst <- audit_intent(intent, ...) + + # call next method + main_audit <- audit_engine( + x, + project = c(intent_lst$main_audit_args$project, project), + user = c(intent_lst$main_audit_args$user, user), + logs_from = logs_from, + logs_to = logs_to, + path = path, + intent_lst$main_audit_args + ) + + # return list + if (is.null(intent_lst$intent_audit)) { + return(main_audit) + } + list(intent = intent_lst$intent_audit, deployment = main_audit) +} + +#' @rdname audit +#' @export +audit.rocrate <- function(x, ..., intent = NULL) { + # return input rocrate object + x +} diff --git a/R/audit_cr8tor.R b/R/audit_cr8tor.R deleted file mode 100644 index 4bb10aa..0000000 --- a/R/audit_cr8tor.R +++ /dev/null @@ -1,97 +0,0 @@ -#' Audit cr8tor project archive -#' -#' This audit loads a cr8tor project archive and generates an RO-Crate object -#' with pre-deployment governance details. This then can be rendered with -#' [rocrate_report()]. -#' -#' @param path Path to cr8tor archive. -#' @param ... Additional arguments for [rocrateR::load_rocrate]. -#' @return RO-Crate audit object -#' -#' @references https://karectl-crates.github.io/cr8tor-metamodel/ -#' @export -audit_cr8tor <- function(path, ...) { - bundle <- load_cr8tor_bundle(path, ...) - - audit <- list( - metadata = extract_cr8tor_metadata(bundle), - integrity = extract_integrity_cr8tor(bundle), - safe_people = extract_safe_people_cr8tor(bundle), - safe_projects = extract_safe_projects_cr8tor(bundle), - safe_data = extract_safe_data_cr8tor(bundle), - safe_settings = extract_safe_settings_cr8tor(bundle), - safe_outputs = extract_safe_outputs_cr8tor(bundle), - user_projects = extract_user_projects_cr8tor(bundle), - user_groups = extract_user_groups_cr8tor(bundle), - groups = extract_groups_cr8tor(bundle), - permissions = extract_permissions_cr8tor(bundle) #, - # lineage = extract_lineage_cr8tor(bundle), - ) - - as_rocrate_audit(audit) -} - -#' Convert audit result into RO-Crate -#' -#' @param audit list returned by audit_cr8tor() -#' @return rocrate object -#' @noRd -as_rocrate_audit <- function(audit) { - rc <- rocrateR::rocrate_5s() - - # Root dataset describing the audit - rc <- rc |> - rocrateR::add_entity_value( - "./", - "name", - "cr8tor 5 Safes Audit", - overwrite = TRUE - ) |> - rocrateR::add_entity_value( - "./", - "description", - "Audit report generated from cr8tor archive", - overwrite = TRUE - ) - - # ---- Safe People ---- - rc <- rc |> - add_group_entities_cr8tor(audit$groups) |> - add_safe_people_entities_cr8tor( - audit$safe_people$users, - audit$user_projects - ) |> - link_people_to_root(audit$safe_people$users$username) - - # ---- Safe Projects ---- - rc <- rc |> - add_safe_project_entities_cr8tor( - audit$safe_projects, - audit$safe_data$tables - ) - - # ---- Safe Data ---- - rc <- rc |> - add_safe_data_entities_cr8tor(audit$safe_data$tables) - - # ---- Permissions ---- - rc <- rc |> - add_permission_entities_cr8tor( - expand_group_permissions_to_users( - perm_tbl = audit$permissions, - membership_tbl = audit$user_groups, - data_tbl = audit$safe_data$tables - ) |> - dedupe_effective_permissions() - ) - - # ---- Safe Settings ---- - rc <- rc |> - add_safe_setting_entities_cr8tor(audit$safe_settings) - - # ---- Safe Outputs ---- - rc <- rc |> - add_safe_output_entities_cr8tor(audit$safe_outputs) - - rc -} diff --git a/R/audit_engine.R b/R/audit_engine.R new file mode 100644 index 0000000..f05ab10 --- /dev/null +++ b/R/audit_engine.R @@ -0,0 +1,170 @@ +#' Audit Engine +#' +#' Internal function to create audits for various back-ends. +#' +#' @param x This can be a connection to a 'DataSHIELD' server (e.g., object with +#' the `opal` class, see [opalr::opal.login()]). Alternatively, a 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 +#' details. +#' @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 +#' (default: `-Inf`, everything up to `logs_to`) +#' @param logs_to Upper limit timestamp to filter out the outputs generated +#' (default: `Inf`, everything from `logs_from` onwards). +#' @param path String with path pointing to the root of the RO-Crate. This will +#' be used to store log files. If not provided, logs will be stored within +#' the RO-Crate returned by this function. +#' +#' @returns Audit RO-Crate with 5 Safes Components. +#' @keywords internal +audit_engine <- function(x, ...) { + UseMethod("audit_engine") +} + +#' @rdname audit_engine +#' @export +audit_engine.cr8tor <- function(x, ...) { + # extract individual components from cr8tor bundle + audit <- list( + metadata = extract_cr8tor_metadata(x), + integrity = extract_integrity_cr8tor(x), + safe_people = extract_safe_people_cr8tor(x), + safe_projects = extract_safe_projects_cr8tor(x), + safe_data = extract_safe_data_cr8tor(x), + safe_settings = extract_safe_settings_cr8tor(x), + safe_outputs = extract_safe_outputs_cr8tor(x), + user_projects = extract_user_projects_cr8tor(x), + user_groups = extract_user_groups_cr8tor(x), + groups = extract_groups_cr8tor(x), + permissions = extract_permissions_cr8tor(x) + ) + + as_rocrate_audit(audit) +} + +#' @rdname audit_engine +#' @export +audit_engine.opal <- function( + x, + ..., + project = NULL, + user = NULL, + logs_from = -Inf, + logs_to = Inf, + path = NULL +) { + # local bindings + name <- principal <- NULL + + # create RO-Create with the 5 safes profile + crate <- rocrateR::rocrate_5s() + + # validate Opal connection + is_opal_admin_con(x) + + # if `project` is missing, then ~extract all project names~ error + if (is.null(project)) { + stop("A `project` name is required!", call. = FALSE) + } + + # extract all data sources to verify `project` contains a valid value. + ds <- opalr::opal.datasources(x) + server_prjs <- ds[, "name"] + idx <- project %in% server_prjs + if (!all(idx)) { + stop( + "The following project", + ifelse(length(idx) == 1, " is ", "s are "), + "not valid: \n", + paste0(" - ", project[!idx], collapse = "\n"), + call. = FALSE + ) + } + + # Safe People ---- + # get users' details + safe_people_tbl <- opalr::oadmin.user_profiles(x, df = FALSE) |> + dplyr::bind_rows() |> + dplyr::rename(name = principal) |> + # exclude system administrators from the report + dplyr::filter(!(tolower(name) %in% c("admin", "administrator"))) + + if (!is.null(user)) { + safe_people_tbl <- safe_people_tbl |> + dplyr::filter(tolower(name) %in% user) + + if (nrow(safe_people_tbl) == 0) { + stop( + sprintf( + "No Safe People details were found for the user: %s!", + paste0("'", user, "'", collapse = ", ") + ), + call. = FALSE + ) + } + } + + crate <- safe_people_tbl$name |> + purrr::reduce( + \(crate, u) { + safe_people( + crate, + connection = x, + user = u, + set_author = FALSE, + set_project = FALSE + ) + }, + .init = crate + ) + + # Safe Projects ---- + crate <- project |> + purrr::reduce( + \(crate, p) { + safe_project(crate, connection = x, project = p) + }, + .init = crate + ) + + # Safe Data ---- + crate <- project |> + purrr::reduce( + \(crate, p) { + safe_data(crate, connection = x, project = p) + }, + .init = crate + ) + + # remove permissions associated with admin users + non_admin_user_ids <- safe_people_tbl$name |> + purrr::map_chr(id_hash, prefix = "#person:") + admin_perm_ents <- crate$`@graph` |> + purrr::keep(\(x) grepl("^#perm:", getElement(x, "@id"))) |> + purrr::discard(\(x) getElement(x, "agent")[[1]] %in% non_admin_user_ids) + crate <- admin_perm_ents |> + purrr::reduce(rocrateR::remove_entity, .init = crate) + + # Safe Settings ---- + crate <- safe_setting(x, rocrate = crate) + + # Safe Outputs ---- + crate <- safe_people_tbl$name |> + purrr::reduce( + \(crate, u) { + safe_output( + crate, + connection = x, + path = path, + user = u, + logs_to = logs_to, + logs_from = logs_from + ) + }, + .init = crate + ) +} diff --git a/R/audit_safe_people.R b/R/audit_safe_people.R deleted file mode 100644 index 80b2bb5..0000000 --- a/R/audit_safe_people.R +++ /dev/null @@ -1,174 +0,0 @@ -#' Audit Safe People details -#' -#' Audit Safe People details from a 'DataSHIELD' server, an RO-Crate object or -#' a file path pointing to an RO-Crate. -#' -#' @inheritParams init -#' @param x This can be a connection to a 'DataSHIELD' server (e.g., object with -#' the `opal` class, see [opalr::opal.login()]). -#' @param ... Other optional arguments, see full documentation for details. -#' @param user String with the user name for which to extract Safe People -#' details. -#' @param project String with project name(s) from which to extra Safe People -#' details. -#' @param logs_from Lower limit timestamp to filter out the outputs generated -#' (default: `-Inf`, everything up to `logs_to`) -#' @param logs_to Upper limit timestamp to filter out the outputs generated -#' (default: `Inf`, everything from `logs_from` onwards). -#' -#' @returns Updated RO-Crate object with Safe People information. -#' @export -#' -# @examples -audit_safe_people <- function(x, ...) { - UseMethod("audit_safe_people") -} - -#' @rdname audit_safe_people -#' @export -audit_safe_people.default <- function(x, ...) { - stop( - "Unknown class, please try with a connection object (e.g., OBiBa's Opal)!" - ) -} - -#' @rdname audit_safe_people -#' @export -audit_safe_people.opal <- function( - x, - ..., - user, - project = NULL, - logs_from = -Inf, - logs_to = Inf, - path = NULL -) { - # local bindings - project_tables_all <- subject <- type <- NULL - - # validate Opal connection - is_opal_admin_con(x) - - # if `project` is missing, then extract all project names - if (is.null(project)) { - # extract all data sources - ds <- opalr::opal.datasources(x) - - project <- ds[, "name"] - } - - suppressWarnings({ - project_tables_all <- x |> - get_project_details(project) - }) - - # get permissions for each table in the project - # get table permissions - project_table_permissions_tbl <- seq_len(nrow(project_tables_all)) |> - lapply(function(i) { - get_table_permissions( - x, - project_tables_all[i, "project"][[1]], - project_tables_all[i, "table"][[1]] - ) - }) |> - dplyr::bind_rows() - - # filter out project permissions for the given user - project_table_permissions_tbl_v2 <- project_table_permissions_tbl |> - dplyr::filter(subject %in% user, type == "user") - - # check if any permission records were found for the current project - if (nrow(project_table_permissions_tbl_v2) == 0) { - stop( - "The given `project`, does not have any permissions set for the given `user`!", - call. = FALSE - ) - } - - # create RO-Create with user, projects and datasets they have access to - safe_people_crate <- rocrateR::rocrate_5s() - - ## add Safe Data and Safe Project details - for (p in unique(project_table_permissions_tbl_v2$project)) { - # filter out tables for the current project - project_tables <- project_table_permissions_tbl_v2 |> - dplyr::filter(project == p) - # add tables for the current project - safe_people_crate <- safe_people_crate |> - dsROCrate::safe_data( - project = p, - tables = project_tables$table, - connection = x - ) - # add project details - safe_people_crate <- safe_people_crate |> - dsROCrate::safe_project(project = p, connection = x) - } - - # add Safe People details - for (i in seq_len(length(user))) { - safe_people_crate <- safe_people_crate |> - dsROCrate::safe_people( - user = user[i], - connection = x, - set_author = FALSE, - set_project = FALSE - ) - } - - # extract Dataset entities from the RO-Crate: @id & name - safe_data_entities_tbl <- safe_people_crate |> - flatten_safe_data() |> - dplyr::rename("table_id" = "id") - - # extract Person entities from the RO-Crate: @id & name - safe_people_entities_tbl <- safe_people_crate |> - flatten_safe_people() |> - dplyr::rename("user_id" = "id") - - ## combine the table permissions with Dataset & People entities' @ids - project_table_permissions_tbl_v3 <- project_table_permissions_tbl |> - dplyr::filter(subject %in% user, type == "user") |> - dplyr::left_join(safe_data_entities_tbl, by = c("table" = "name")) |> - dplyr::left_join(safe_people_entities_tbl, by = c("subject" = "name")) |> - dplyr::rename(user = subject) - - ## generate user permission entities and add to the RO-Crate - user_perm_entity_lst <- project_table_permissions_tbl_v3 |> - purrr::pmap(user_perm_entity) |> - purrr::list_c() - - # ignore warnings about existing permission entities - suppressWarnings({ - safe_people_crate <- user_perm_entity_lst |> - purrr::reduce( - rocrateR::add_entity, - overwrite = TRUE, - .init = safe_people_crate - ) - }) - - # add Safe Setting details - safe_people_crate <- x |> - extract_safe_setting(rocrate = safe_people_crate) - - # add Safe Output details - safe_people_crate <- x |> - extract_safe_output( - path = path, - user = safe_people_entities_tbl$name, - logs_to = logs_to, - logs_from = logs_from, - rocrate = safe_people_crate - ) - - # attach input args as attributes to the RO-Crate - attr(safe_people_crate, "audit_type") <- "Safe People" - attr(safe_people_crate, "path") <- path - attr(safe_people_crate, "project") <- project - attr(safe_people_crate, "user") <- user - - # return new RO-Crate - return(safe_people_crate) -} diff --git a/R/audit_safe_project.R b/R/audit_safe_project.R deleted file mode 100644 index af8048c..0000000 --- a/R/audit_safe_project.R +++ /dev/null @@ -1,177 +0,0 @@ -#' Audit Safe Project details -#' -#' Audit Safe Project details from a 'DataSHIELD' server, an RO-Crate object or -#' a file path pointing to an RO-Crate. -#' -#' @inheritParams audit_safe_people -#' @param ... Other optional arguments, see full documentation for details. -#' -#' @returns Updated RO-Crate object with Safe Project information. -#' @export -audit_safe_project <- function(x, ...) { - UseMethod("audit_safe_project") -} - -#' @rdname audit_safe_project -#' @export -audit_safe_project.default <- function(x, ...) { - stop( - "Unknown class, please try with a connection object (e.g., OBiBa's Opal)!" - ) -} - -#' @rdname audit_safe_project -#' @export -audit_safe_project.opal <- function( - x, - ..., - project = NULL, - logs_from = -Inf, - logs_to = Inf, - path = NULL -) { - # local bindings - name <- principal <- project_tables_all <- subject <- table <- type <- NULL - - # validate Opal connection - is_opal_admin_con(x) - # validate_opal_con(x) - - # if `project` is missing, then extract all project names - if (is.null(project)) { - # extract all data sources - ds <- opalr::opal.datasources(x) - - project <- ds[, "name"] - } - - suppressWarnings({ - project_tables_all <- x |> - get_project_details(project) - }) - - # add check to determine if project information was found: - if (nrow(project_tables_all) == 0) { - stop( - paste0( - "No data details were found for given project", - ifelse(length(project) == 1, "", "s"), - "!" - ), - call. = FALSE - ) - } - # get permissions for each table in the project - # get table permissions - project_table_permissions_tbl <- seq_len(nrow(project_tables_all)) |> - lapply(function(i) { - get_table_permissions( - x, - project_tables_all[i, "project"][[1]], - project_tables_all[i, "table"][[1]] - ) - }) |> - dplyr::bind_rows() - - # create RO-Create with projects and datasets, plus information of users that - # have access to them - safe_project_crate <- rocrateR::rocrate_5s() - - ## add Safe Data and Safe Project details - for (p in unique(project_table_permissions_tbl$project)) { - # filter out tables for the current project - project_tables <- project_table_permissions_tbl |> - dplyr::filter(project == p) - # add tables for the current project - safe_project_crate <- safe_project_crate |> - dsROCrate::safe_data( - project = p, - tables = project_tables$table, - connection = x - ) - # add project details - safe_project_crate <- safe_project_crate |> - dsROCrate::safe_project(project = p, connection = x) - } - - # get users' details - safe_people_tbl <- opalr::opal.get(x, "/system/subject-profiles/") |> - dplyr::bind_rows() |> - dplyr::rename(name = principal) |> - # exclude system administrators from the report - dplyr::filter(!(tolower(name) %in% c("admin", "administrator"))) |> - # filter out users that don't have access to the given project(s) - dplyr::filter(name %in% project_table_permissions_tbl$subject) - - # filter out table permissions based on the users found previously: - project_table_permissions_tbl <- project_table_permissions_tbl |> - dplyr::filter(subject %in% safe_people_tbl$name) - - # add Safe People details - for (i in seq_len(nrow(safe_people_tbl))) { - safe_project_crate <- safe_project_crate |> - dsROCrate::safe_people( - user = safe_people_tbl$name[i], - connection = x, - set_author = FALSE, - set_project = FALSE - ) - } - - # extract Dataset entities from the RO-Crate: @id & name - safe_data_entities_tbl <- safe_project_crate |> - flatten_safe_data() |> - dplyr::rename("table_id" = "id") - - # extract Person entities from the RO-Crate: @id & name - safe_people_entities_tbl <- safe_project_crate |> - flatten_safe_people() |> - dplyr::rename("user_id" = "id") - - ## combine the table permissions with Dataset & People entities' @ids - project_table_permissions_tbl_v2 <- project_table_permissions_tbl |> - dplyr::left_join(safe_data_entities_tbl, by = c("table" = "name")) |> - dplyr::left_join(safe_people_entities_tbl, by = c("subject" = "name")) |> - dplyr::rename(user = subject) - - ## generate user permission entities and add to the RO-Crate - user_perm_entity_lst <- project_table_permissions_tbl_v2 |> - purrr::pmap(user_perm_entity) |> - purrr::list_c() - # ignore warnings about existing permission entities - suppressWarnings({ - safe_project_crate <- user_perm_entity_lst |> - purrr::reduce( - rocrateR::add_entity, - overwrite = TRUE, - .init = safe_project_crate - ) - }) - - # add Safe Setting details - safe_project_crate <- x |> - extract_safe_setting(rocrate = safe_project_crate) - - # add Safe Output details - for (u in safe_people_entities_tbl$name) { - # # suppress warnings, as some users might not have logs in the given period - # suppressWarnings({ - safe_project_crate <- x |> - extract_safe_output( - path = path, - user = u, - logs_to = logs_to, - logs_from = logs_from, - rocrate = safe_project_crate, - ) - # }) - } - - # attach input args as attributes to the RO-Crate - attr(safe_project_crate, "audit_type") <- "Safe Project" - attr(safe_project_crate, "path") <- path - attr(safe_project_crate, "project") <- project - - # return new RO-Crate - return(safe_project_crate) -} diff --git a/R/audit_study.R b/R/audit_study.R deleted file mode 100644 index b158602..0000000 --- a/R/audit_study.R +++ /dev/null @@ -1,95 +0,0 @@ -#' Audit Study details -#' -#' Audit Study details from a 'DataSHIELD' server, an RO-Crate object or -#' a file path pointing to an RO-Crate. -#' -#' @inheritParams audit_safe_people -#' @param ... Other optional arguments, see full documentation for details. -#' -#' @returns Updated RO-Crate object with Study information. -#' @export -audit_study <- function(x, ...) { - UseMethod("audit_study") -} - -#' @rdname audit_study -#' @export -audit_study.default <- function(x, ...) { - stop( - "Unknown class, please try a named list of connections (e.g., OBiBa's Opal)", - " or a single connection object!" - ) -} - -#' @rdname audit_study -#' @export -audit_study.list <- function( - x, - ..., - project = NULL, - logs_from = -Inf, - logs_to = Inf, - path = NULL -) { - # local bindings - name <- principal <- project_tables_all <- subject <- table <- type <- NULL - - utils::capture.output( - suppressMessages(suppressWarnings({ - safe_project_reports <- x |> - purrr::map(function(conn) { - audit_study( - conn, - project = project, - logs_from = logs_from, - logs_to = logs_to, - path = path - ) - }) - })), - file = nullfile() - ) - - # attach input args as attributes to the RO-Crate - attr(safe_project_reports, "audit_type") <- "Study" - attr(safe_project_reports, "path") <- path - attr(safe_project_reports, "project") <- project - - # return list with new RO-Crates (one per connection given) - return(safe_project_reports) -} - -#' @rdname audit_study -#' @export -audit_study.opal <- function( - x, - ..., - project = NULL, - logs_from = -Inf, - logs_to = Inf, - path = NULL -) { - # local bindings - name <- principal <- project_tables_all <- subject <- table <- type <- NULL - - utils::capture.output( - suppressMessages(suppressWarnings({ - safe_project_reports <- x |> - audit_safe_project( - project = project, - logs_from = logs_from, - logs_to = logs_to, - path = path - ) - })), - file = nullfile() - ) - - # attach input args as attributes to the RO-Crate - attr(safe_project_reports, "audit_type") <- "Study" - attr(safe_project_reports, "path") <- path - attr(safe_project_reports, "project") <- project - - # return list with new RO-Crates (one per connection given) - return(safe_project_reports) -} diff --git a/R/dsROCrate-package.R b/R/dsROCrate-package.R index be3f8f9..aff741d 100644 --- a/R/dsROCrate-package.R +++ b/R/dsROCrate-package.R @@ -2,6 +2,7 @@ "_PACKAGE" ## usethis namespace: start -#' @import methods +# @import methods +# @importClassesFrom DSMolgenisArmadillo ArmadilloCredentials ## usethis namespace: end NULL diff --git a/R/dsROCrate.R b/R/dsROCrate.R index a3a62f3..0fd90c3 100644 --- a/R/dsROCrate.R +++ b/R/dsROCrate.R @@ -1,4 +1,6 @@ -#' Initialise Five Safes RO-Crate +#' Initialise a Five Safes RO-Crate +#' +#' Creates a new RO-Crate configured for Five Safes auditing. #' #' @param x This can be a connection to a 'DataSHIELD' server (e.g., object with #' the `opal` class, see [opalr::opal.login()]), an RO-Crate @@ -27,7 +29,13 @@ #' the Safe People, it must include `@id` and `name` entries. Alternatively, #' this can be a string with the `name` of the current user. #' -#' @returns Five Safes RO-Crate. +#' @returns Five Safes RO-Crate object. +#' +#' @references +#' Wilkinson, M., Dumontier, M., Aalbersberg, I. et al. (2016) The FAIR Guiding +#' Principles for scientific data management and stewardship. Sci Data 3, +#' 160018. https://doi.org/10.1038/sdata.2016.18 +#' #' @export init <- function(x, ...) { UseMethod("init") diff --git a/R/print.R b/R/print.R new file mode 100644 index 0000000..931afeb --- /dev/null +++ b/R/print.R @@ -0,0 +1,49 @@ +#' @export +print.cr8tor_bundle <- function(x, ...) { + msg <- "" + + is_valid_roc <- function(x) { + inherits(x$rocrate, "rocrate") && + rocrateR::is_rocrate(x$rocrate, error = FALSE) + } + + if (is_valid_roc(x)) { + msg <- c(msg, " \U2714 Valid RO-Crate") + } else { + msg <- c(msg, " \U2716 Invalid RO-Crate") + } + + # project metadata (safe extraction) + proj <- tryCatch( + x$resources[["governance/cr8-governance.yaml"]]$project, + error = function(e) NULL + ) + + if (!is.null(proj)) { + msg <- c( + msg, + paste0(" Project: ", proj$name, " (", proj$reference, ")") + ) + } + + # counts + n_resources <- length(x$resources) + n_entities <- length(x$rocrate$`@graph` %||% list()) + n_actions <- length(proj$actions %||% list()) + n_users <- length( + x$resources[["governance/cr8-governance.yaml"]]$users %||% list() + ) + + msg <- c( + msg, + " Contents:", + paste0(" - Entities: ", n_entities), + paste0(" - Resources: ", n_resources), + paste0(" - Actions: ", n_actions), + paste0(" - Users: ", n_users) + ) + + message(paste0(msg, collapse = "\n")) + + invisible(x) +} diff --git a/R/rocrate_report.R b/R/report.R similarity index 86% rename from R/rocrate_report.R rename to R/report.R index 965cae9..6f3dfda 100644 --- a/R/rocrate_report.R +++ b/R/report.R @@ -3,17 +3,17 @@ #' @param x This can be an RO-Crate ([rocrate][rocrateR::rocrate()] class) or a #' string with the path to an RO-Crate. #' @param ... Other optional arguments. See the full documentation, -#' [`?dsROCrate::rocrate_report`][rocrate_report()]. +#' [`?dsROCrate::report`][report()]. #' #' @returns RO-Crate report as markdown (.md) file and/or HTML. #' @export -rocrate_report <- function(x, ...) { - UseMethod("rocrate_report") +report <- function(x, ...) { + UseMethod("report") } -#' @rdname rocrate_report +#' @rdname report #' @export -rocrate_report.character <- function( +report.character <- function( x, ..., title = "DataSHIELD Report", @@ -28,11 +28,11 @@ rocrate_report.character <- function( max_line_length = 200 ) { # attempt loading the RO-Crate - rocrate <- load_rocrate(x) + rocrate <- rocrateR::load_rocrate(x) # call the next generic method rocrate |> - rocrate_report( + report( title = title, filepath = filepath, render = render, @@ -45,9 +45,9 @@ rocrate_report.character <- function( ) } -#' @rdname rocrate_report +#' @rdname report #' @export -rocrate_report.default <- function(x, ...) { +report.default <- function(x, ...) { stop( "Unknown class, please try either a file path or", " an object with `rocrate` class!" @@ -55,9 +55,9 @@ rocrate_report.default <- function(x, ...) { } #' @param study_name String with the study name. -#' @rdname rocrate_report +#' @rdname report #' @export -rocrate_report.list <- function( +report.list <- function( x, ..., study_name, @@ -73,7 +73,7 @@ rocrate_report.list <- function( max_line_length = 200 ) { # local bindings - id <- name <- permission <- project <- server <- user <- NULL + asset <- id <- name <- permission <- project <- server <- user <- NULL # validate that all the objects in the list, `x`, are valid RO-Crates sapply(x, rocrateR::is_rocrate) @@ -81,7 +81,7 @@ rocrate_report.list <- function( # generate individual reports for each RO-Crate report_outputs <- lapply( x, - rocrate_report, + report, title = title, filepath = filepath, render = FALSE, @@ -94,131 +94,40 @@ rocrate_report.list <- function( ) # combine reports ---- - ## Safe People ----- - safe_people_all <- tryCatch( - { - report_outputs |> - # extract each component per server - lapply(getElement, name = "safe_people") |> - # attach the server name as a new column - purrr::imap(~ dplyr::mutate(.x, server = .y)) |> - # combine rows - dplyr::bind_rows() - }, - error = function(e) { - NULL - } - ) - ## Safe Data ---- - safe_data_all <- tryCatch( - { - report_outputs |> - # extract each component per server - lapply(getElement, name = "safe_data") |> - # attach the server name as a new column - purrr::imap(~ dplyr::mutate(.x, server = .y)) |> - # combine rows - dplyr::bind_rows() - }, - error = function(e) { - NULL - } - ) - ## Safe Projects ---- - safe_project_all <- tryCatch( - { - report_outputs |> - # extract each component per server - lapply(getElement, name = "safe_project") |> - # attach the server name as a new column - purrr::imap(~ dplyr::mutate(.x, server = .y)) |> - # combine rows - dplyr::bind_rows() - }, - error = function(e) { - NULL - } - ) - ## Safe Settings ---- - safe_setting_all <- tryCatch( - { - report_outputs |> - # extract each component per server - lapply(getElement, name = "safe_setting") |> - # attach the server name as a new column - purrr::imap(~ dplyr::mutate(.x, server = .y)) |> - # combine rows - dplyr::bind_rows() - }, - error = function(e) { - NULL - } - ) - ## Safe Outputs ---- - safe_output_all <- tryCatch( - { - report_outputs |> - # extract each component per server - lapply(getElement, name = "safe_output") |> - # attach the server name as a new column - purrr::imap(~ dplyr::mutate(.x, server = .y)) |> - # combine rows - dplyr::bind_rows() - }, - error = function(e) { - NULL - } - ) - ## Safe Data permissions (optional) ---- - safe_data_permissions_all <- tryCatch( - { - report_outputs |> - # extract each component per server - lapply(getElement, name = "safe_data_permissions") |> - # attach the server name as a new column - purrr::imap(~ dplyr::mutate(.x, server = .y)) |> - # combine rows - dplyr::bind_rows() - }, - error = function(e) { - NULL - } - ) + ## Safe People + safe_people_all <- .extract_srvr_comp(report_outputs, "safe_people") + ## Safe Data + safe_data_all <- .extract_srvr_comp(report_outputs, "safe_data") + ## Safe Projects + safe_project_all <- .extract_srvr_comp(report_outputs, "safe_project") + ## Safe Settings + safe_setting_all <- .extract_srvr_comp(report_outputs, "safe_setting") + ## Safe Outputs + safe_output_all <- .extract_srvr_comp(report_outputs, "safe_output") + ## Safe Data permissions (optional) + safe_data_permissions_all <- report_outputs |> + .extract_srvr_comp("safe_data_permissions") ## overview table ---- overview_data_all <- tibble::tibble() ## combine aggregated data to generate new overview table - safe_project_data_all <- safe_project_all |> - dplyr::rename(project_id = id) |> - dplyr::left_join( - safe_data_all |> - dplyr::rename(table_id = id), - by = c("table" = "name", "server" = "server") - ) + safe_project_data_all <- safe_project_all #|> + # dplyr::rename(asset = asset_name) + # if data permissions were found, then combine with project-data details and # generate new overview table if (!is.null(safe_data_permissions_all)) { overview_data_all <- safe_data_permissions_all |> - dplyr::left_join( - safe_people_all |> - dplyr::rename(user_id = id), - by = c("user_id", "server") - ) |> - dplyr::left_join(safe_project_data_all, by = c("table_id", "server")) |> - dplyr::select( - server, - project, - table, - permission, - user = name - ) + dplyr::left_join(safe_people_all, by = c("person_id", "server")) |> + dplyr::left_join(safe_project_data_all, by = c("asset_id", "server")) |> + dplyr::select(server, project, asset, permission, user = name) # if any Safe Outputs were found in the inputs, include in the overview if (!is.null(safe_output_all) && nrow(safe_output_all)) { overview_data_all <- overview_data_all |> dplyr::left_join( safe_output_all, - by = c("server", "project", "table", "user") + by = c("server", "project", "asset", "user") ) } } else { @@ -261,7 +170,7 @@ rocrate_report.list <- function( report_contents <- c( .markdown_report_header(title, overview_data_all, overview_lst$diag_path), tidy_overview_tbl |> - # # tidy up duplicated values in `project` and `table` + # # tidy up duplicated values in `project` and `asset` # dplyr::mutate( # Project = unfill_vec(Project), # Data = unfill_vec(Data) @@ -340,7 +249,6 @@ rocrate_report.list <- function( stop("The format `", doc_format, "` is not valid! Try 'html' or 'pdf'.") } } else { - print(overview_lst$diag_lst) return(invisible( list( overview_diagram = overview_lst$diag_lst, @@ -387,9 +295,9 @@ rocrate_report.list <- function( #' overview's diagram (default: `NULL`, estimated based on number of nodes). #' @param max_line_length Integer with the maximum number of characters per line #' in the RO-Crate to be printed in the report. -#' @rdname rocrate_report +#' @rdname report #' @export -rocrate_report.rocrate <- function( +report.rocrate <- function( x, ..., title = "DataSHIELD Report", @@ -404,9 +312,9 @@ rocrate_report.rocrate <- function( max_line_length = 200 ) { # local bindings ---- - actionStatus <- description <- fx <- table <- NULL - encodingFormat <- id <- name <- project <- table_id <- table_name <- NULL - timestamp <- type <- user_id <- user <- NULL + actionStatus <- asset <- asset_id <- description <- fx <- NULL + encodingFormat <- id <- name <- permission <- perm_id <- project <- NULL + table_name <- timestamp <- type <- person_id <- user <- NULL # validate RO-Crate ---- rocrateR::is_rocrate(x) @@ -546,36 +454,31 @@ rocrate_report.rocrate <- function( ### extract (if available) table with user permissions user_perm_tbl <- flatten_user_perm_entity(user_perm_entity_lst) ### extract table with Safe People details - safe_people_tbl <- flatten_safe_people(safe_people_rocrate) |> - dplyr::rename(user_id = id) + safe_people_tbl <- flatten_safe_people(safe_people_rocrate) ### extract table with Safe Project details safe_project_tbl <- flatten_safe_project(safe_project_rocrate) ### extract table with Safe Data details - safe_data_tbl <- flatten_safe_data(safe_data_rocrate) |> - dplyr::rename(table_id = id, table_name = name) + safe_data_tbl <- flatten_safe_data(safe_data_rocrate) if (!is.null(user_perm_tbl) && nrow(user_perm_tbl) > 0) { overview_tbl <- user_perm_tbl |> # combine with Safe People details - dplyr::left_join(safe_people_tbl, by = "user_id") |> - # drop unused columns - dplyr::select(-id, -actionStatus, -description) |> + dplyr::left_join(safe_people_tbl, by = "person_id") |> + # subset columns of interest + dplyr::select(perm_id, person_id, name, asset_id, permission) |> # combine with Safe Data details - dplyr::left_join(safe_data_tbl, by = c("table_id")) |> + dplyr::left_join(safe_data_tbl, by = "asset_id") |> # combine with Safe Project details - dplyr::left_join(safe_project_tbl, by = c("table_name" = "table")) |> - # drop unused columns - dplyr::select(-id, -user_id, -table_id, -type) |> - dplyr::rename(table = table_name) + dplyr::left_join(safe_project_tbl, by = c("asset_id", "asset")) #|> + # # drop unused columns + # dplyr::select(-id, -person_id, -asset_id, -type) |> + # dplyr::rename(asset = asset_name) } else { overview_tbl <- flatten_safe_people(safe_people_rocrate) |> - dplyr::select(-id) |> - dplyr::bind_cols( - flatten_safe_project(safe_project_rocrate) |> - dplyr::select(-id), - flatten_safe_data(safe_data_rocrate) |> - dplyr::select(-id) |> - dplyr::rename(table = name) - ) + purrr::pmap(function(person_id, name, ...) { + tibble::tibble(person_id, name) |> + dplyr::bind_cols(flatten_safe_project(safe_project_rocrate)) + }) |> + purrr::list_c() } # attempt extracting list of functions executed by the users from Safe Outputs @@ -596,15 +499,15 @@ rocrate_report.rocrate <- function( safe_output_tbl_v2 <- safe_output_tbl |> dplyr::mutate( project = gsub("(?=\\.).*$", "", table, perl = TRUE), - table = gsub("^.*(?<=\\.)", "", table, perl = TRUE) + asset = gsub("^.*(?<=\\.)", "", table, perl = TRUE) ) |> - dplyr::distinct(project, table, user, fx, timestamp) + dplyr::distinct(project, asset, user, fx, timestamp) # append the list of functions to the overview table overview_tbl <- overview_tbl |> dplyr::left_join( safe_output_tbl_v2, - by = c("project" = "project", "table" = "table", "name" = "user") + by = c("project" = "project", "asset" = "asset", "name" = "user") ) |> # replace 'NA' in fx & timestamp with empty string dplyr::mutate( @@ -695,7 +598,6 @@ rocrate_report.rocrate <- function( stop("The format `", doc_format, "` is not valid! Try 'html' or 'pdf'.") } } else { - print(overview_lst$diag_lst) return(invisible( list( overview_diagram = overview_lst$diag_lst, @@ -725,13 +627,34 @@ rocrate_report.rocrate <- function( ) } +.extract_srvr_comp <- function(x, component) { + tryCatch( + { + x |> + # extract each component per server + purrr::map(component) |> + # lapply(getElement, name = component) |> + # filter out NULL elements + purrr::keep(\(x) !is.null(x)) |> + # attach the server name as a new column + purrr::imap(~ dplyr::mutate(.x, server = .y)) |> + # combine rows + dplyr::bind_rows() + }, + error = function(e) { + NULL + } + ) +} + #' Create diagram for RO-Crate overview #' #' @param overview_tbl Data frame with overview details for the RO-Crate. -#' @inheritParams rocrate_report +#' @inheritParams report #' #' @returns Diagram object #' @keywords internal +#' @noRd .overview_diagram <- function( overview_tbl, include_user_perm, @@ -742,11 +665,11 @@ rocrate_report.rocrate <- function( diag_height ) { # local bindings - fx <- name <- permission <- project <- table <- NULL + fx <- name <- permission <- project <- asset <- NULL ## initialise `vars` and `labelvar` - vars <- c("project", "table") - labelvar <- c(project = "Project", table = "Data") + vars <- c("project", "asset") + labelvar <- c(project = "Project", asset = "Data") # check if `overview_tbl` has `permission` field AND include_user_perm = TRUE if ("permission" %in% colnames(overview_tbl) && include_user_perm) { @@ -848,13 +771,14 @@ rocrate_report.rocrate <- function( #' #' @returns Data frame with tidy overview table. #' @keywords internal +#' @noRd .tidy_overview <- function(overview_tbl, include_user_perm) { # local bindings fx <- permission <- timestamp <- NULL ## initialise `vars` and `varslab` - vars <- c("project", "table") - varslab <- c("Project" = "project", "Data" = "table") + vars <- c("project", "asset") + varslab <- c("Project" = "project", "Data" = "asset") # check if `overview_tbl` has `permission` field AND include_user_perm = TRUE if ("permission" %in% colnames(overview_tbl) && include_user_perm) { @@ -924,10 +848,11 @@ rocrate_report.rocrate <- function( #' Generate Markdown report's header #' #' @inheritParams .overview_diagram -#' @inheritParams rocrate_report +#' @inheritParams report #' #' @returns String with report's header #' @keywords internal +#' @noRd .markdown_report_header <- function(title, overview_tbl, diagram_filepath) { # initialise variables for the report header unique_users_vct <- unique(c( @@ -1034,6 +959,7 @@ rocrate_report.rocrate <- function( #' #' @returns String with updated Markdown report. #' @keywords internal +#' @noRd .markdown_report_body <- function( report_contents, overview_tbl, @@ -1122,6 +1048,7 @@ rocrate_report.rocrate <- function( #' #' @returns String with update Markdown report. #' @keywords internal +#' @noRd .markdown_report_rocrate <- function( report_contents, rocrate, @@ -1170,6 +1097,7 @@ rocrate_report.rocrate <- function( #' #' @returns String with data frame rendered with `kable`. #' @keywords internal +#' @noRd .break_tibble <- function(df, varname) { # local bindings temp <- NULL diff --git a/R/safe_people.R b/R/safe_people.R index fb19e7d..4426eb5 100644 --- a/R/safe_people.R +++ b/R/safe_people.R @@ -93,8 +93,9 @@ safe_people.opal <- function( validate_opal_con(x) # attempt to retrieve project entity + project_id <- id_hash("#project:", project) safe_project_entity <- rocrate |> - .get_entity(type = "Project") + .get_entity(id = project_id, type = "Project") # initialise empty user entity user_entity <- NULL @@ -108,6 +109,14 @@ safe_people.opal <- function( id = c(getElement(user, "@id"), getElement(user, "id")), type = "Person", name = c(getElement(user, "name"), getElement(user, "username")), + givenName = c( + getElement(user, "givenName"), + getElement(user, "firstname") + ), + familyName = c( + getElement(user, "familyName"), + getElement(user, "surname") + ), affiliation = list(`@id` = c(getElement(user, "affiliation"))) ) } else { diff --git a/R/safe_project.R b/R/safe_project.R index bbd550c..c7cd4f8 100644 --- a/R/safe_project.R +++ b/R/safe_project.R @@ -103,7 +103,7 @@ safe_project.opal <- function( user = NULL ) { # declare local bindings - created <- lastUpdate <- NULL + created <- lastUpdate <- type <- NULL # x is a valid opal connection object validate_opal_con(x) @@ -121,34 +121,84 @@ safe_project.opal <- function( # check if the given `project` exists project_exists(x, project = project) + # create project @id + project_id <- id_hash(project_id_suffix, project) + # retrieve details associated to `project` project_details_tbl <- opalr::opal.project(x, project) # filter out asset entities associated with the project based on the # value for `asset_id_suffix`. - project_asset_entities <- rocrate$`@graph` |> + crate_asset_entities <- rocrate$`@graph` |> purrr::keep(\(x) grepl(paste0("^", asset_id_suffix), x$`@id`)) # create project entity timestamps <- getElement(project_details_tbl, "timestamps") project_entity <- rocrateR::entity( - id = id_hash(project_id_suffix, project), + id = project_id, type = "Project", name = getElement(project_details_tbl, "name"), dateCreated = getElement(timestamps, "created"), dateModified = getElement(timestamps, "lastUpdate"), - hasPart = purrr::map(project_asset_entities, ~ list("@id" = .x$`@id`)) + hasPart = if (length(crate_asset_entities) > 0) { + purrr::map(crate_asset_entities, ~ list("@id" = .x$`@id`)) + } else { + NULL + } ) - # if no tables are associated to this project, then drop `hasPart` - if (length(project_asset_entities) == 0) { - project_entity$hasPart <- NULL - } - # add new project entity to the RO-Crate rocrate <- rocrate |> rocrateR::add_entity(project_entity, overwrite = TRUE) + # Opal permissions ---- + perms <- opalr::opal.project_perm(x, project) + + project_users <- perms |> + dplyr::filter(type == "user") |> + purrr::pmap_chr(\(subject, ...) subject) |> + stats::na.omit() + + # link existing Person entities ---- + people <- .get_entity(rocrate, type = "Person") + + if (!is.null(people)) { + for (p in people) { + user <- p$name + + if (user %in% project_users) { + rocrate <- append_entity_ref( + rocrate, + id = p[["@id"]], + key = "memberOf", + ref_id = project_id + ) + } + } + } + + # link existing asset entities ---- + # extract assets for the given project + project_tbl_assets <- get_project_assets(x, project, "tables") + project_res_assets <- get_project_assets(x, project, "resources") + # combine assets + proj_assets_tbl <- dplyr::bind_rows(project_tbl_assets, project_res_assets) + + # filter crate's assets based on the assets associated to the project + crate_asset_entities <- crate_asset_entities |> + purrr::keep(\(x) getElement(x, "name") %in% proj_assets_tbl$name) + + if (!is.null(crate_asset_entities) && length(crate_asset_entities) > 0) { + for (ass in crate_asset_entities) { + rocrate <- append_entity_ref( + rocrate, + id = ass[["@id"]], + key = "isPartOf", + ref_id = project_id + ) + } + } + # attach input arguments as attributes attr(rocrate, "connection") <- x attr(rocrate, "path") <- path @@ -199,30 +249,27 @@ safe_project.rocrate <- function( } # S4 methods ---- -#' @aliases safe_project,armadillo-method +#' @method safe_project ArmadilloCredentials +#' @rdname safe_project #' @export -setMethod( - "safe_project", - signature(x = "armadillo"), - function( - x, - ..., - profile = "default", - project = NULL, - rocrate = rocrateR::rocrate_5s(), - asset_id_suffix = "#asset:", - project_id_suffix = "#project:", - path = NULL, - resources = NULL, - tables = NULL, - user = NULL - ) { - # check if the given `project` exists - project_exists(x, project = project) - - # retrieve details associated to `project` - project_details_tbl <- MolgenisArmadillo::armadillo.get_projects_info() |> - purrr::list_c() |> - tibble::as_tibble() - } -) +safe_project.ArmadilloCredentials <- function( + x, + ..., + profile = "default", + project = NULL, + rocrate = rocrateR::rocrate_5s(), + asset_id_suffix = "#asset:", + project_id_suffix = "#project:", + path = NULL, + resources = NULL, + tables = NULL, + user = NULL +) { + # check if the given `project` exists + project_exists(x, project = project) + + # retrieve details associated to `project` + project_details_tbl <- MolgenisArmadillo::armadillo.get_projects_info() |> + purrr::list_c() |> + tibble::as_tibble() +} diff --git a/R/safe_setting.R b/R/safe_setting.R index 7272781..c0d5aca 100644 --- a/R/safe_setting.R +++ b/R/safe_setting.R @@ -204,6 +204,7 @@ safe_setting.opal <- function( name = Package, version = Version, description = Description |> + gsub(pattern = "[[:space:]]+", replacement = " ") |> trimws() ) }) diff --git a/R/utils-audit.R b/R/utils-audit.R new file mode 100644 index 0000000..5be4ee8 --- /dev/null +++ b/R/utils-audit.R @@ -0,0 +1,38 @@ +audit_intent <- function(intent, excluded_args = c("project", "user"), ...) { + # if `intent` is NOT NULL, audit this object + intent_audit <- if (!is.null(intent)) { + audit(intent, ...) + } else { + NULL + } + + # list of additional args + main_audit_args <- list(...) + + # check if `intent_audit` is not NULL + if (!is.null(intent_audit)) { + # extract Project(s) and People from the `intent` audit crate + safe_project_tbl <- intent_audit |> + flatten_safe_project() + safe_people_tbl <- intent_audit |> + flatten_safe_people() + + main_audit_args <- exclude_args(main_audit_args, excluded = excluded_args) + + main_audit_args <- c( + main_audit_args, + project = safe_project_tbl$project, + user = safe_people_tbl$name + ) + } + + list(intent_audit = intent_audit, main_audit_args = main_audit_args) +} + +exclude_args <- function(..., excluded) { + # capture additional args + args <- list(...) + arg_names <- names(args) + # exclude args that shouldn't be passed to the next function + args[!(arg_names %in% excluded)] +} diff --git a/R/utils-connection.R b/R/utils-connection.R index 8273ab6..3517549 100644 --- a/R/utils-connection.R +++ b/R/utils-connection.R @@ -22,7 +22,7 @@ parse_user_profiles.opal <- function(x, ..., user) { principal <- userInfo <- NULL # get user profiles and filter by the current user - user_prof_tbl <- opalr::opal.get(x, "/system/subject-profiles/") |> + user_prof_tbl <- opalr::oadmin.user_profiles(x, df = FALSE) |> dplyr::bind_rows() |> dplyr::filter(principal %in% user) # extract (if available) `userInfo` which contains additional details @@ -45,15 +45,12 @@ parse_user_profiles.opal <- function(x, ..., user) { } # S4 methods ---- -#' @aliases parse_user_profiles,armadillo-method -#' @family Armadillo -setMethod( - "parse_user_profiles", - signature(x = "armadillo"), - function(x, ..., user) { - message("PLACEHOLDER!") - } -) +#' @method parse_user_profiles ArmadilloCredentials +#' @rdname parse_user_profiles +#' @export +parse_user_profiles.ArmadilloCredentials <- function(x, ..., user) { + message("PLACEHOLDER!") +} #' Verify if project exists #' @@ -86,10 +83,9 @@ project_exists <- function(x, ...) { project_exists.opal <- function(x, ..., project) { if (!opalr::opal.project_exists(x, project)) { stop( - paste0( - "The given `project = '", - project, - "'` was not found in the given Opal connection!" + sprintf( + "The `project = '%s'` was not found in the given Opal connection!", + project ), call. = FALSE ) @@ -97,11 +93,11 @@ project_exists.opal <- function(x, ..., project) { } # S4 methods ---- -#' @aliases project_exists,armadillo-method +#' @method project_exists ArmadilloCredentials +#' @rdname project_exists +#' @export #' @family Armadillo -setMethod( - "project_exists", - signature(x = "armadillo"), +project_exists.ArmadilloCredentials <- function( x, ..., @@ -109,13 +105,11 @@ setMethod( ) { if (!(project %in% MolgenisArmadillo::armadillo.list_projects())) { stop( - paste0( - "The given `project = '", - project, - "'` was not found in the given Armadillo connection!" + sprintf( + "The `project = '%s'` was not found in the given Armadillo connection!", + project ), call. = FALSE ) } } -) diff --git a/R/utils-cr8tor.R b/R/utils-cr8tor.R index 5045db5..9b681e6 100644 --- a/R/utils-cr8tor.R +++ b/R/utils-cr8tor.R @@ -17,7 +17,7 @@ add_safe_people_entities_cr8tor <- function(rc, people_tbl, membership_tbl) { for (i in seq_len(nrow(people_tbl))) { p <- people_tbl[i, ] - person_id <- paste0("#person:", digest::digest(p$username)) + person_id <- id_hash("#person:", p$username) # projects user belongs to projs <- membership_tbl |> @@ -26,20 +26,17 @@ add_safe_people_entities_cr8tor <- function(rc, people_tbl, membership_tbl) { unique() memberOf <- purrr::map(projs, \(pr) { - list(`@id` = paste0("#project:", pr)) + list(`@id` = id_hash("#project:", pr)) }) - display_name <- paste(p$given_name, p$family_name) - if (is.na(display_name) || trimws(display_name) == "") { - display_name <- p$username - } - rc <- rc |> rocrateR::add_entity( rocrateR::entity( id = person_id, type = "Person", - name = display_name, + name = p$username, + givenName = p$given_name, + familyName = p$family_name, email = p$email, affiliation = p$affiliation, memberOf = memberOf @@ -58,35 +55,34 @@ add_safe_people_entities_cr8tor <- function(rc, people_tbl, membership_tbl) { #' @param proj_tbl Tibble with project metadata. #' Columns: id, name. #' @param data_tbl Tibble with dataset metadata. -#' Columns: project, table. +#' Columns: project, asset. #' #' @return Updated RO-Crate #' @noRd add_safe_project_entities_cr8tor <- function(rc, proj_tbl, data_tbl) { # local bindings - project <- NULL + asset <- project <- NULL now <- format(Sys.time(), "%Y-%m-%dT%H:%M:%OS3Z", tz = "UTC") for (i in seq_len(nrow(proj_tbl))) { - project_id <- proj_tbl$id[i] - project_name <- proj_tbl$name[i] - project_eid <- paste0("#project:", project_id) + project_id <- proj_tbl$project_id[i] + project <- proj_tbl$name[i] ds_ids <- data_tbl |> dplyr::filter(project == project_id) |> - dplyr::pull(table) |> + dplyr::pull(asset) |> unique() |> - (\(x) paste0("#dataset:", x))() + (\(x) id_hash("#asset:", paste0(project, x)))() has_part <- purrr::map(ds_ids, \(x) list(`@id` = x)) rc <- rc |> rocrateR::add_entity( rocrateR::entity( - id = project_eid, + id = id_hash("#project:", project), type = "Project", - name = project_name, + name = project, dateCreated = now, dateModified = now, hasPart = has_part @@ -104,21 +100,21 @@ add_safe_project_entities_cr8tor <- function(rc, proj_tbl, data_tbl) { #' #' @param rc RO-Crate object, see [rocrateR::rocrate]. #' @param tbl Tibble with dataset metadata. -#' Columns: project, table. +#' Columns: project, asset. #' #' @return Updated RO-Crate #' @noRd add_safe_data_entities_cr8tor <- function(rc, tbl) { for (i in seq_len(nrow(tbl))) { - dataset_id <- paste0("#dataset:", tbl$table[i]) - project_id <- paste0("#project:", tbl$project[i]) + asset_id <- id_hash("#asset:", paste0(tbl$project[i], tbl$asset[i])) + project_id <- id_hash("#project:", tbl$project[i]) rc <- rc |> rocrateR::add_entity( rocrateR::entity( - id = dataset_id, + id = asset_id, type = "Dataset", - name = tbl$table[i], + name = tbl$asset[i], isPartOf = list(`@id` = project_id) ) ) @@ -140,7 +136,7 @@ add_safe_data_entities_cr8tor <- function(rc, tbl) { add_group_entities_cr8tor <- function(rc, groups_tbl) { for (i in seq_len(nrow(groups_tbl))) { g <- groups_tbl[i, ] - gid <- paste0("#group:", g$group_id) + gid <- id_hash("#group:", g$group_id) rc <- rc |> rocrateR::add_entity( @@ -149,7 +145,7 @@ add_group_entities_cr8tor <- function(rc, groups_tbl) { type = "Organization", name = g$group_id, description = g$description, - parentOrganization = list(`@id` = paste0("#project:", g$project)) + parentOrganization = list(`@id` = id_hash("#project:", g$project)) ) ) } @@ -159,12 +155,12 @@ add_group_entities_cr8tor <- function(rc, groups_tbl) { #' Add Permission entities (Opal-level) #' -#' Expands user-table permission matrix into RO-Crate Action entities. +#' Expands user-asset permission matrix into RO-Crate Action entities. #' Uses `user_perm_entity()` to generate Read/Write/Control actions. #' #' @param rc RO-Crate object, see [rocrateR::rocrate]. #' @param perm_expanded_tbl Tibble with user permissions expanded. -#' Columns: username, table, permission. +#' Columns: username, asset, permission. #' #' @return Updated RO-Crate #' @noRd @@ -172,14 +168,14 @@ add_permission_entities_cr8tor <- function(rc, perm_expanded_tbl) { for (i in seq_len(nrow(perm_expanded_tbl))) { row <- perm_expanded_tbl[i, ] - user_id <- paste0("#person:", digest::digest(row$username)) - table_id <- paste0("#dataset:", row$table) + person_id <- id_hash("#person:", row$username) + asset_id <- id_hash("#asset:", paste0(row$project, row$asset)) ents <- user_perm_entity( - user = row$username, - user_id = user_id, - table = row$table, - table_id = table_id, + person = row$username, + person_id = person_id, + asset = row$asset, + asset_id = asset_id, permission = row$permission ) @@ -200,7 +196,7 @@ add_safe_setting_entities_cr8tor <- function(rc, tbl) { rc <- rc |> rocrateR::add_entity( rocrateR::entity( - id = paste0("#setting:", nm), + id = id_hash("#setting:", nm), type = "PropertyValue", name = nm, value = as.character(tbl[[i]]) @@ -227,14 +223,84 @@ add_safe_output_entities_cr8tor <- function(rc, tbl) { ) } -#' Keep strongest permission per user-table pair +#' Convert audit result into RO-Crate +#' +#' @param audit list returned by audit.cr8tor() +#' @return rocrate object +#' @noRd +as_rocrate_audit <- function(audit) { + rc <- rocrateR::rocrate_5s() + + # Root dataset describing the audit + rc <- rc |> + rocrateR::add_entity_value( + "./", + "name", + "cr8tor 5 Safes Audit", + overwrite = TRUE + ) |> + rocrateR::add_entity_value( + "./", + "description", + "Audit report generated from cr8tor archive", + overwrite = TRUE + ) + + # ---- Safe People ---- + rc <- rc |> + add_group_entities_cr8tor(audit$groups) |> + add_safe_people_entities_cr8tor( + audit$safe_people$users, + audit$user_projects + ) |> + link_people_to_root(audit$safe_people$users$username) + + # ---- Safe Projects ---- + rc <- rc |> + add_safe_project_entities_cr8tor( + audit$safe_projects, + audit$safe_data$assets + ) + + # ---- Safe Data ---- + rc <- rc |> + add_safe_data_entities_cr8tor(audit$safe_data$assets) + + # ---- Permissions ---- + rc <- rc |> + add_permission_entities_cr8tor( + expand_group_permissions_to_users( + perm_tbl = audit$permissions |> + dplyr::left_join( + audit$safe_projects, + by = c("project" = "project_id") + ), + membership_tbl = audit$user_groups, + data_tbl = audit$safe_data$assets + ) |> + dedupe_effective_permissions() + ) + + # ---- Safe Settings ---- + rc <- rc |> + add_safe_setting_entities_cr8tor(audit$safe_settings) + + # ---- Safe Outputs ---- + rc <- rc |> + add_safe_output_entities_cr8tor(audit$safe_outputs) + + rc +} + + +#' Keep strongest permission per user-asset pair #' -#' @param perm_tbl Tibble with username, table, permission. +#' @param perm_tbl Tibble with username, asset, permission. #' @return Deduplicated tibble #' @noRd dedupe_effective_permissions <- function(perm_tbl) { # local bindings - permission <- strength <- username <- NULL + asset <- permission <- strength <- username <- NULL strength_order <- c( "view", "view-values", @@ -247,13 +313,13 @@ dedupe_effective_permissions <- function(perm_tbl) { dplyr::mutate( strength = match(permission, strength_order) ) |> - dplyr::group_by(username, table) |> + dplyr::group_by(username, asset) |> dplyr::slice_max(strength, n = 1, with_ties = FALSE) |> dplyr::ungroup() |> dplyr::select(-strength) } -#' Expand cr8tor group permissions to user-table permissions +#' Expand cr8tor group permissions to user-asset permissions #' #' Converts: #' Group → Project permissions @@ -264,7 +330,7 @@ dedupe_effective_permissions <- function(perm_tbl) { #' @param membership_tbl Tibble from extract_user_groups_cr8tor(). #' @param data_tbl Tibble from extract_safe_data_cr8tor()$tables. #' -#' @return Tibble with username, project, table, permission +#' @return Tibble with username, project, asset, permission #' @noRd expand_group_permissions_to_users <- function( perm_tbl, @@ -272,7 +338,7 @@ expand_group_permissions_to_users <- function( data_tbl ) { # local bindings - project <- role <- user <- username <- NULL + asset <- name <- project <- role <- user <- username <- NULL # map cr8tor role to Opal permission role_to_permission <- function(role) { @@ -305,10 +371,11 @@ expand_group_permissions_to_users <- function( # 3. Map roles to permissions perm_tables |> + dplyr::mutate(project = name) |> dplyr::transmute( username, project, - table, + asset, permission = role_to_permission(role) ) |> dplyr::distinct() @@ -355,19 +422,6 @@ extract_integrity_cr8tor <- function(bundle) { ) } -#' Extract lineage matrix (User × Project × Table) -#' -#' @param bundle cr8tor_bundle -#' @return data.frame -#' @noRd -extract_lineage_cr8tor <- function(bundle) { - users <- extract_safe_people_cr8tor(bundle)$users$username - proj <- extract_safe_projects_cr8tor(bundle)$name - tables <- extract_safe_data_cr8tor(bundle)$tables$table - - expand.grid(user = users, project = proj, table = tables) -} - #' Extract Safe Data (datasets & tables) #' #' @param bundle cr8tor_bundle @@ -383,9 +437,11 @@ extract_safe_data_cr8tor <- function(bundle) { } ing_yaml <- yaml::yaml.load(paste(ing$content[[1]], collapse = "\n")) + # extract groups and projects + group_tbl <- extract_groups_cr8tor(bundle) proj_tbl <- extract_safe_projects_cr8tor(bundle) - tables <- ing_yaml$datasets |> + assets <- ing_yaml$datasets |> purrr::map(function(ds) { # project name from locations yaml_project <- ds$locations[[1]]$opal_project_name %||% NA_character_ @@ -396,7 +452,7 @@ extract_safe_data_cr8tor <- function(bundle) { tibble::tibble( project = project_id, dataset = ds$name, - table = tbl$name, + asset = tbl$name, n_cols = length(tbl$columns) ) }) |> @@ -405,8 +461,8 @@ extract_safe_data_cr8tor <- function(bundle) { purrr::list_c() tibble::tibble( - tables = tables, - n_tables = nrow(tables) + assets = assets, + n_assets = nrow(assets) ) } @@ -488,6 +544,13 @@ extract_user_groups_cr8tor <- function(bundle) { } extract_user_projects_cr8tor <- function(bundle) { + # local bindings + description <- group_id <- NULL + # extract group details + grp_tbl <- extract_groups_cr8tor(bundle) + # extract project details + prj_tbl <- extract_projects_cr8tor(bundle) + # extract user docs user_docs <- bundle$resources[ grepl("user-.*\\.ya?ml$", names(bundle$resources)) ] @@ -499,16 +562,20 @@ extract_user_projects_cr8tor <- function(bundle) { if (length(groups) == 0) { return(NULL) } - purrr::map(groups, function(g) { - grp <- g$value - project <- sub("-(admin|analyst)$", "", grp) + usr_grp_tbl <- purrr::map(groups, function(g) { tibble::tibble( username = username, - project = project + group_id = g$value ) }) |> purrr::list_c() + + # combine user x groups x projects + usr_grp_tbl |> + dplyr::left_join(grp_tbl, by = "group_id") |> + dplyr::select(-description, -group_id) |> + dplyr::left_join(prj_tbl, by = "project") }) |> purrr::list_c() |> dplyr::distinct() @@ -580,11 +647,20 @@ extract_permissions_cr8tor <- function(bundle) { stringsAsFactors = FALSE ) |> tibble::as_tibble() - # tibble::tibble( - # group = y$metadata$name %||% NA_character_, - # project = y$spec$projects %||% NA, - # role = y$spec$role %||% NA - # ) + }) |> + purrr::list_c() +} + +extract_projects_cr8tor <- function(bundle) { + prj_docs <- bundle$resources[ + grepl("project-.*\\.ya?ml$", names(bundle$resources)) + ] + + purrr::map(prj_docs, function(doc) { + tibble::tibble( + project = doc$metadata$name, + description = doc$spec$description %||% NA_character_ + ) }) |> purrr::list_c() } @@ -600,12 +676,16 @@ extract_safe_projects_cr8tor <- function(bundle) { # deployment spec proj_docs <- bundle$resources[ - grepl("project-.*\\.ya?ml$", names(bundle$resources)) + grepl( + sprintf("project-%s.*\\.ya?ml$", proj$identifier), + names(bundle$resources) + ) ] tibble::tibble( - id = proj$identifier %||% proj$name %||% NA_character_, - name = proj$name %||% NA_character_, + project_id = proj$identifier, + name = proj$identifier %||% NA_character_, + description = proj$name %||% NA_character_, uuid = proj$`@id` %||% NA_character_, deployment_defined = length(proj_docs) > 0 ) @@ -655,7 +735,7 @@ find_bagit_root <- function(path) { link_people_to_root <- function(rc, usernames) { authors <- lapply(usernames, \(u) { - list(`@id` = paste0("#person:", digest::digest(u))) + list(`@id` = id_hash("#person:", u)) }) rocrateR::add_entity_value( @@ -674,20 +754,20 @@ link_people_to_root <- function(rc, usernames) { #' * resources/ → deployment & governance YAML specs. #' * config.toml → platform configuration. #' -#' @param path Path to cr8tor ZIP archive. +#' @param x Path to cr8tor ZIP archive. #' @param ... Unused. #' #' @return Object of class `cr8tor`. -#' @export -load_cr8tor_bundle <- function(path, ...) { +#' @keywords internal +load_cr8tor_bundle <- function(x, ...) { tmp <- tempfile("cr8tor_") dir.create(tmp, recursive = TRUE, showWarnings = FALSE) - utils::unzip(path, exdir = tmp) + utils::unzip(x, exdir = tmp) # load RO-Crate layer rocrate <- rocrateR::load_rocrate( - path, #file.path(tmp, "bagit"), + x, #file.path(tmp, "bagit"), load_content = TRUE, ... ) @@ -719,11 +799,14 @@ load_cr8tor_bundle <- function(path, ...) { config = config, root = tmp_root ), - class = "cr8tor" + class = c("cr8tor", "cr8tor_bundle", "list") ) } -map_project_name_to_id <- function(project_name, proj_tbl) { - idx <- which(proj_tbl$name == project_name) - if (length(idx)) proj_tbl$id[idx[1]] else project_name +map_project_name_to_id <- function(project, proj_tbl) { + idx <- which(proj_tbl$description == project) + if (length(idx) == 0) { + idx <- which(proj_tbl$name == project) + } + if (length(idx)) proj_tbl$project_id[idx[1]] else project } diff --git a/R/utils-opal.R b/R/utils-opal.R index 64f26fc..c3149c7 100644 --- a/R/utils-opal.R +++ b/R/utils-opal.R @@ -1,3 +1,15 @@ +#' Add Asset Permissions to RO-Crate +#' +#' Creates a Permission entity describing dataset-level access for a person +#' within a project and links it to the relevant assets. +#' +#' @param rocrate RO-Crate object, see [rocrateR::rocrate]. +#' @param project Project identifier. +#' @param person User identifier. +#' @param assets Vector of strings with asset identifiers. +#' +#' @return Updated RO-Crate. +#' #' @noRd add_asset_permissions_to_crate <- function( rocrate, @@ -12,41 +24,37 @@ add_asset_permissions_to_crate <- function( asset_id <- id_lookup[[asset$name]] asset_type <- asset$asset_type - perms <- get_asset_permissions( - x, - project, - asset_type, - asset$name - ) + # retrieve asset permissions + perms <- get_asset_permissions(x, project, asset_type, asset$name) if (is.null(perms)) { next } + # iterate through person permissions for an asset for (j in seq_len(nrow(perms))) { - user <- perms$user[j] + person <- perms$person[j] permission <- perms$permission[j] - user_id <- id_hash("#user:", user) + person_id <- id_hash("#person:", person) # create permission entities perm_ents <- user_asset_perm_entities( - user = user, - user_id = user_id, - asset_name = asset$name, + person = person, + person_id = person_id, + asset = asset$name, asset_id = asset_id, permission = permission, asset_type = asset_type ) # add permission entities - for (ent in perm_ents) { - rocrate <- rocrateR::add_entity( - rocrate, - ent, - overwrite = TRUE - ) - } + rocrate <- purrr::reduce( + perm_ents, + rocrateR::add_entity, + overwrite = TRUE, + .init = rocrate + ) } } @@ -133,7 +141,7 @@ get_asset_permissions <- function(x, project, asset_type, name) { } tibble::tibble( - user = perms$subject, + person = perms$subject, permission = perms$permission ) } @@ -153,7 +161,7 @@ get_project_assets <- function(x, project, type = c("tables", "resources")) { project_exists(x, project = project) if (type == "tables") { - prj <- opalr::opal.get(x, "datasource", project, "tables") + prj <- opalr::opal.tables(x, project, df = FALSE) if (length(prj) == 0) { return(NULL) @@ -182,7 +190,7 @@ get_project_assets <- function(x, project, type = c("tables", "resources")) { meta = vector("list", length(prj)) ) } else if (type == "resources") { - res <- opalr::opal.get(x, "project", project, "resources") + res <- opalr::opal.resources(x, project, df = FALSE) if (length(res) == 0) { return(NULL) @@ -344,7 +352,7 @@ get_table_permissions <- function(x, project, tables) { #' @keywords internal flatten_user_perm_entity <- function(x) { # local bindings - agent <- object <- table_id <- user_id <- NULL + agent <- object <- asset_id <- person_id <- NULL # flatten list of entities x_tbl <- x |> @@ -357,14 +365,14 @@ flatten_user_perm_entity <- function(x) { # edit tibble with entities x_tbl |> dplyr::rename( - id = "@id", + perm_id = "@id", type = "@type", - user_id = agent, - table_id = object + person_id = agent, + asset_id = object ) |> dplyr::mutate( - user_id = unlist(user_id), - table_id = unlist(table_id), + person_id = unlist(person_id), + asset_id = unlist(asset_id), ) |> # add permission field, based on `type` dplyr::mutate( @@ -563,9 +571,9 @@ update_project_datasets <- function(rocrate, project, ds_ids) { #' @noRd user_asset_perm_entities <- function( - user, - user_id, - asset_name, + person, + person_id, + asset, asset_id, permission, asset_type = c("table", "resource") @@ -574,35 +582,42 @@ user_asset_perm_entities <- function( # reuse existing function by aliasing "table" args user_perm_entity( - user = user, - user_id = user_id, - table = asset_name, - table_id = asset_id, + person = person, + person_id = person_id, + asset = asset, + asset_id = asset_id, permission = permission ) } -#' Create user permission entities +#' Create user/person permission entities #' -#' @param user String with user name. -#' @param user_id String with user `@id`. -#' @param table String with dataset/table name. -#' @param table_id String with dataset/table `@id`. +#' @param person String with person name/username. +#' @param person_id String with person `@id`. +#' @param asset String with dataset/table/resource name. +#' @param asset_id String with dataset/table `@id`. #' @param permission String with permission ('view', 'view-values', 'edit', #' 'edit-values' OR 'administrate'). #' @param ... Other additional values. #' #' @returns List of [rocrateR::entity] objects #' @keywords internal -user_perm_entity <- function(user, user_id, table, table_id, permission, ...) { +user_perm_entity <- function( + person, + person_id, + asset, + asset_id, + permission, + ... +) { # set local bindings description <- type <- NULL action_status <- "PotentialActionStatus" - agent <- list(list(`@id` = user_id)) - object <- list(list(`@id` = table_id)) + agent <- list(list(`@id` = person_id)) + object <- list(list(`@id` = asset_id)) # create combined @id - comb_id <- paste0("#perm:", digest::digest(paste0(user, "-", table))) + comb_id <- id_hash("#perm:", paste0(person, "-", asset)) # update combine @id, @type and description based on permission if (permission == "view") { diff --git a/R/utils-rocrate.R b/R/utils-rocrate.R index a151a47..845aff3 100644 --- a/R/utils-rocrate.R +++ b/R/utils-rocrate.R @@ -42,72 +42,3 @@ load_content <- function(rocrate, roc_path) { return(rocrate) } - -#' Load RO-Crate from file -#' -#' @param x String with path to RO-Crate -#' -#' @returns RO-Crate object. -#' @keywords internal -load_rocrate <- function(x) { - # check if the given file, `x`, exists - if (!file.exists(x)) { - stop("The given file:\n `", x, "`\nis not a valid path!", call. = FALSE) - } - - # initialise local variables - roc_path <- rocrate <- NULL - - # check if the given path points to an RO-Crate bag (zip file) - if (grepl("zip$", tolower(x))) { - # create temp directory to extract contents of RO-Crate - tempdir_name <- file.path(tempdir(), digest::digest(Sys.time())) - dir.create(tempdir_name, recursive = TRUE) - on.exit(unlink(tempdir_name, force = TRUE, recursive = TRUE)) - - # unbag RO-Crate - roc_path <- tryCatch( - { - rocrateR::unbag_rocrate(x, output = tempdir_name, quiet = TRUE) - }, - error = function(e) { - x - } - ) - } else { - roc_path <- x - } - - # load RO-Crate - rocrate <- tryCatch( - { - # list JSON files, if roc_path points to a directory - if (dir.exists(roc_path)) { - roc_path_files <- list.files( - roc_path, - pattern = "[json|JSON]$", - recursive = TRUE, - full.names = TRUE - ) - roc_path_files <- roc_path_files[1] - roc_path <- dirname(roc_path_files) - } else { - roc_path_files <- roc_path - roc_path <- dirname(roc_path) - } - rocrateR::read_rocrate(roc_path_files) - }, - error = function(e) { - NULL - } - ) - - # check if the RO-Crate was loaded correctly - if (is.null(rocrate)) { - stop("Unable to load an RO-Crate from the given file:\n `", x, "`") - } - - # check if any of the entities with `@type = 'File'` have empty `content` - rocrate <- rocrate |> - load_content(roc_path = roc_path) -} diff --git a/R/utils-rocrateR.R b/R/utils-rocrateR.R index f819722..4183e15 100644 --- a/R/utils-rocrateR.R +++ b/R/utils-rocrateR.R @@ -1,3 +1,39 @@ +#' Append entity reference +#' +#' Adds a reference to an existing property while +#' preserving existing values and avoiding duplicates. +#' +#' @param rocrate RO-Crate object. +#' @param id String with `@id` of the entity. +#' @param key String with property/key of the entity. +#' @param ref_id String with reference `@id`. +#' +#' @returns Updated RO-Crate object. +#' @keywords internal +append_entity_ref <- function(rocrate, id, key, ref_id) { + entity <- rocrateR::get_entity(rocrate, id = id)[[1]] + + current <- entity[[key]] + + existing_ids <- if (!is.null(current)) { + getElement(current, "@id") %||% purrr::map_chr(current, "@id") + } else { + character() + } + + ids <- unique(c(existing_ids, ref_id)) + + value <- purrr::map(ids, ~ list("@id" = .x)) + + rocrateR::add_entity_value( + rocrate, + id = id, + key = key, + value = value, + overwrite = TRUE + ) +} + #' Wrapper for [rocrateR::get_entity()] #' #' This wrapper is used to suppress warning messages like diff --git a/R/utils-safe_data.R b/R/utils-safe_data.R index 553c759..5b3765d 100644 --- a/R/utils-safe_data.R +++ b/R/utils-safe_data.R @@ -17,26 +17,17 @@ extract_safe_data.opal <- function(x, ..., rocrate = rocrateR::rocrate_5s()) { # extract all data sources ds <- opalr::opal.datasources(x) - # cycle through the data source (x) and data project details - for (i in seq_len(nrow(ds))) { - project_name <- ds[i, "name"] - project_tables <- get_project_tables(x, project_name) - if ( - !is.na(project_name) && - !is.null(project_name) && - length(project_tables) > 0 - ) { - rocrate <- rocrate |> - safe_data( - project = project_name, - tables = project_tables, - connection = x - ) - } - } + # extract project names and ignore NAs + projects <- ds$name[!is.na(ds$name) & !is.null(ds$name)] - # return RO-Crate with Safe Data details - return(rocrate) + # create RO-Crate with Safe Data details for the projects found on the server + purrr::reduce( + projects, + \(crate, p) { + safe_data(crate, connection = x, project = p) + }, + .init = rocrate + ) } #' @param id (Optional) Vector with `@id` strings for Safe Data entity(ies) @@ -47,46 +38,39 @@ extract_safe_data.rocrate <- function( x, ..., id = NULL, + asset_id_suffix = "#asset:", rocrate = rocrateR::rocrate_5s() ) { # validate RO-Crate rocrateR::is_rocrate(x) - # extract Dataset entities - entities_lst <- .get_entity(x, type = "Dataset") + # filter out asset entities associated with the project based on the + # value for `asset_id_suffix`. + entities_lst <- x$`@graph` |> + purrr::keep(\(x) grepl(paste0("^", asset_id_suffix), x$`@id`)) # if `id` was provided, then filter out only those entities if (!is.null(id)) { - idx <- entities_lst |> - sapply(\(x) getElement(x, "@id") %in% id) - entities_lst <- entities_lst[idx] + entities_lst <- entities_lst |> + purrr::keep(\(x) getElement(x, "@id") %in% id) } - # remove root entity, ./ - idx <- entities_lst |> - sapply(\(x) getElement(x, "@id") == "./") - entities_lst[idx] <- NULL - # check if any entities were found if (length(entities_lst) == 0) { stop("No matching entities were found!", call. = FALSE) } else { message( length(entities_lst), - " 'Dataset' entit", + " asset entit", ifelse(length(entities_lst) == 1, "y was", "ies were"), " found!" ) } - # add Dataset entities to the RO-Crate + # add entities to the RO-Crate suppressWarnings({ - rocrate <- rocrate |> - rocrateR::add_entity(entities_lst, verbose = FALSE) + purrr::reduce(entities_lst, rocrateR::add_entity, .init = rocrate) }) - - # return RO-Crate with the Safe Data details - return(rocrate) } #' Flatten object with Safe Data details @@ -96,7 +80,7 @@ extract_safe_data.rocrate <- function( #' @param id Vector of strings with the `@id`s for the datasets to be extracted. #' If not provided, extract all entities with `@type = 'Dataset'`. #' -#' @returns Data frame with safe data details. +#' @returns Data frame with Safe Data details. #' @rdname flatten_safe_data #' @keywords internal flatten_safe_data <- function(x, ...) { @@ -111,28 +95,36 @@ flatten_safe_data.default <- function(x, ...) { #' @rdname flatten_safe_data #' @export -flatten_safe_data.rocrate <- function(x, ..., id = NULL) { +flatten_safe_data.rocrate <- function( + x, + ..., + id = NULL, + asset_id_suffix = "#asset:" +) { + # local bindings + asset_id <- person_id <- NULL + tryCatch( { - # extract Dataset entities - entities_tbl <- x |> - .get_entity(type = "Dataset") |> + # extract asset entities + entities_tbl <- x$`@graph` |> + purrr::keep(\(x) grepl(paste0("^", asset_id_suffix), x$`@id`)) |> # extract @id and name for each entity lapply(function(ent) { tibble::tibble( - id = getElement(ent, "@id"), - name = getElement(ent, "name") + asset_id = getElement(ent, "@id"), + asset = getElement(ent, "name") ) }) |> # combine all rows dplyr::bind_rows() |> # filter out root (./) entity - dplyr::filter(id != "./") + dplyr::filter(asset_id != "./") # if `id` is provided, then only keep those entities if (!is.null(id)) { entities_tbl <- entities_tbl |> - dplyr::filter(id %in% !!id) + dplyr::filter(asset_id %in% !!id) } # return dataset entities diff --git a/R/utils-safe_people.R b/R/utils-safe_people.R index da607cf..827da6f 100644 --- a/R/utils-safe_people.R +++ b/R/utils-safe_people.R @@ -18,7 +18,7 @@ extract_safe_people.opal <- function(x, ..., rocrate = rocrateR::rocrate_5s()) { name <- principal <- NULL # extract all users - opal_users <- opalr::opal.get(x, "/system/subject-profiles/") |> + opal_users <- opalr::oadmin.user_profiles(x, df = FALSE) |> dplyr::bind_rows() |> dplyr::rename(name = principal) |> # exclude system administrators from the report @@ -125,6 +125,8 @@ flatten_safe_people.default <- function(x, ...) { #' @rdname flatten_safe_people #' @export flatten_safe_people.rocrate <- function(x, ..., id = NULL) { + # local bindings + person_id <- NULL tryCatch( { # extract Person entities @@ -133,7 +135,7 @@ flatten_safe_people.rocrate <- function(x, ..., id = NULL) { # extract @id and name for each entity lapply(function(ent) { tibble::tibble( - id = getElement(ent, "@id"), + person_id = getElement(ent, "@id"), name = getElement(ent, "name"), given_name = c( getElement(ent, "givenName"), @@ -152,7 +154,7 @@ flatten_safe_people.rocrate <- function(x, ..., id = NULL) { # if `id` is provided, then only keep those entities if (!is.null(id)) { entities_tbl <- entities_tbl |> - dplyr::filter(id %in% !!id) + dplyr::filter(person_id %in% !!id) } # return dataset entities diff --git a/R/utils-safe_project.R b/R/utils-safe_project.R index 12c025b..1437d1e 100644 --- a/R/utils-safe_project.R +++ b/R/utils-safe_project.R @@ -24,11 +24,11 @@ extract_safe_project.opal <- function( # cycle through the data source (x) and extract project details for (i in seq_len(nrow(ds))) { - project_name <- ds[i, "name"] - if (!is.na(project_name) && !is.null(project_name)) { + project <- ds[i, "name"] + if (!is.na(project) && !is.null(project)) { suppressWarnings({ rocrate <- rocrate |> - safe_project(project = project_name, connection = x) + safe_project(project = project, connection = x) }) } } @@ -135,26 +135,27 @@ flatten_safe_project.rocrate <- function(x, ..., y = x) { has_part <- getElement(ent, "hasPart") |> unlist() project_id <- getElement(ent, "@id") - project_name <- getElement(ent, "name") + project <- getElement(ent, "name") - # if the current Project entity has any Datasets, extract them + # if the current Project entity has any assets, extract them if (!is.null(has_part)) { - # extract datasets by @id - table_names <- y |> - flatten_safe_data(id = has_part) |> - # extract names - getElement("name") - - return(tibble::tibble( - id = project_id, - project = project_name, - table = table_names - )) + # extract assets by @id + assets_tbl <- y |> + flatten_safe_data(id = has_part) + + proj_assets_tbl <- tibble::tibble( + project_id = project_id, + project = project + ) |> + dplyr::bind_cols(assets_tbl) + + return(proj_assets_tbl) } return(tibble::tibble( - id = project_id, - project = project_name, - table = NA + project_id = project_id, + project = project, + asset_id = NA, + asset = NA )) }) |> dplyr::bind_rows() diff --git a/R/utils-show.R b/R/utils-show.R deleted file mode 100644 index 02f7aa1..0000000 --- a/R/utils-show.R +++ /dev/null @@ -1,21 +0,0 @@ -#' @include ArmadilloCredentials-class.R -NULL - -setMethod( - "show", - "ArmadilloCredentials", - function(object) { - expiration_timestamp <- object@expires_at - is_expired <- Sys.time() > expiration_timestamp - cat("\n") - if (is_expired) { - cat(" Connection expired!\n") - cat("\nYou must login again:\n") - cat(" `dsROCrate::armadillo_login(SERVER)`\n") - } else { - cat(" auth type: ", object@auth_type, "\n", sep = "") - cat(" token type:", object@token_type, "\n") - cat(" expires at:", format(object@expires_at, tz = "UTC"), "\n") - } - } -) diff --git a/R/zzz.R b/R/zzz.R new file mode 100644 index 0000000..67d4658 --- /dev/null +++ b/R/zzz.R @@ -0,0 +1,3 @@ +.onLoad <- function(libname, pkgname) { + # methods::setOldClass("ArmadilloCredentials") +} diff --git a/README.Rmd b/README.Rmd index 15f4b8c..526c123 100644 --- a/README.Rmd +++ b/README.Rmd @@ -10,561 +10,98 @@ knitr::opts_chunk$set( comment = "#>", fig.path = "man/figures/README-", out.width = "100%", + cache = FALSE, screenshot.opts = list(vwidth = 2000, vheight = 600, zoom = 3, selector = "div.html-widget") ) - -# save the built-in output hook -hook_output <- knitr::knit_hooks$get("output") -hook_message <- knitr::knit_hooks$get("message") - -# define a truncation helper -truncate_lines_tail <- function(x, lines) { - x <- unlist(strsplit(x, "\n")) - more <- "..." - if (length(lines) == 1) { - if (length(x) > lines) { - # truncate the output, but add `more` - # x <- c(head(x, n = lines), more) - x <- c(more, tail(x, n = lines)) - } - } else { - x <- c(more, x[lines], more) - } - paste(c(x, ""), collapse = "\n") -} - -# set the new hooks -knitr::knit_hooks$set( - output = function(x, options) { - lines <- options$out.lines - if (is.null(lines)) return(hook_output(x, options)) - x <- truncate_lines_tail(x, lines) - return(hook_output(x, options)) - }, - message = function(x, options) { - lines <- options$out.lines - if (is.null(lines)) return(hook_message(x, options)) - x <- truncate_lines_tail(x, lines) - return(hook_message(x, options)) - } -) - -# helper function to get the number of rows of an RO-Crate -rocrate_lines <- function(rocrate) { - rocrate_txt(rocrate) |> - length() -} - -# helper function to read RO-Crate as text file -rocrate_txt <- function(rocrate) { - # create tmp file to write a JSON file with the output - tmp_file <- tempfile(fileext = ".json") - on.exit(unlink(tmp_file, force = TRUE)) - # write RO-Crate in tmp file - rocrateR::write_rocrate(rocrate, tmp_file) - # read RO-Crate as text file - readLines(tmp_file) -} ``` -# dsROCrate: 'DataSHIELD' RO-Crate Wrapper Functions logo +# dsROCrate: 'DataSHIELD' RO-Crate Governance Functions logo -[![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental) [![CRAN status](https://www.r-pkg.org/badges/version/dsROCrate)](https://CRAN.R-project.org/package=dsROCrate) -[![Codecov test coverage](https://codecov.io/gh/DataSHIELD-5S/dsROCrate/graph/badge.svg)](https://app.codecov.io/gh/DataSHIELD-5S/dsROCrate) +[![R-CMD-check](https://github.com/FederatedMethods/dsROCrate/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/FederatedMethods/dsROCrate/actions/workflows/R-CMD-check.yaml) + +[![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental) -The goal of dsROCrate is to provide functions to wrap elements from a -'DataSHIELD' analysis into an RO-Crate. - -## 1. Installation - -You can install the development version of dsROCrate from [GitHub](https://github.com/DataSHIELD-5S/dsROCrate/) with: - -``` r -# install.packages("pak") -pak::pak("DataSHIELD-5S/dsROCrate") -``` - -## 2. Creating your first RO-Crate -In this example, we will be using OBiBa's -[Opal](https://opaldoc.obiba.org/en/latest/index.html) as the _back-end_ for -DataSHIELD. Another option would be MOLGENIS' [Armadillo](https://github.com/molgenis/molgenis-service-armadillo/). - -### 2.1. Connect to an Opal Server -#### Setup -For instructions on how to set up a local server for DataSHIELD, see the -following vignette: - -```{r, eval = FALSE} -vignette("deploy-local-datashield-server-with-opal") -``` - -Here we will use OBiBa's Opal demo server: https://opal-demo.obiba.org/ which -can be accessed with the following login credentials: - -```{r} -# define global variables -## Opal server access -USERNAME <- "administrator" -USERPASS <- "password" -SERVER <- "https://opal-demo.obiba.org" -## Credentials for `dsuser` -### NOTE: this is only used to simulate an analysis and generate logs -DSUSERPASS <- "P@ssw0rd" -``` +## Overview -Next, define global variables used in generating the RO-Crate, such as project -name, table references (within the project) and user identifiers. - -```{r} -## Five safes variables -PEOPLE <- "dsuser" -PROJECT <- "CNSIM" -TABLES <- c("CNSIM1") -``` +`{dsROCrate}` provides tools to capture, structure and audit DataSHIELD analyses using the RO-Crate standard. +It enables reproducible governance of federated analyses by packaging inputs, outputs, metadata, and audit information into a consistent, portable format. -#### Open connection +## Key Features -Once the credentials and Five Safes variables are configured,we can start a -new session on the opal server with the following command: +* Create RO-Crates for DataSHIELD workflows +* Capture project metadata, data references and analysis outputs +* Record and audit disclosure control processes +* Support reproducibility and governance in federated environments -```{r} -# login to local server with `USERNAME` and `USERPASS`. -o <- opalr::opal.login( - username = USERNAME, - password = USERPASS, - url = SERVER -) +## Installation +You can install the released version of `{dsROCrate}` from [CRAN](https://cran.r-project.org/package=dsROCrate) with: -print(o) +``` r +install.packages("dsROCrate") ``` -### 2.2. Create a basic RO-Crate - -To create a basic RO-Crate, we will use the -[`{rocrateR}`](https://github.com/ResearchObject/ro-crate-r) package. This -package can be installed with the following command: +And the development version from [GitHub](https://github.com/FederatedMethods/dsROCrate/) with: -```{r, eval = FALSE, echo = TRUE} +``` r # install.packages("pak") -pak::pak("rocrateR") -``` - -Then, a basic RO-Crate can be created with the following command: - -```{r} -basic_rocrate <- rocrateR::rocrate_5s() -``` - -Note that this RO-Crate uses the [5s-crate](https://trefx.uk/5s-crate/0.4/) -profile. - -```{r} -print(basic_rocrate) -``` - -### 2.3. Add the _Five Safes_ Elements -#### Safe Data -To add details for Safe Data, use the function `dsROCrate::safe_data()`. - -```{r, out.lines=-(rocrate_lines(basic_rocrate) - 2)} -basic_rocrate <- o |> - dsROCrate::safe_data(rocrate = basic_rocrate, - project = PROJECT, - tables = TABLES) - -print(basic_rocrate) # note that the output will be truncated +pak::pak("FederatedMethods/dsROCrate@dev") ``` -#### Safe Project -To add details for Safe Project, use the function `dsROCrate::safe_project()`. +## Quick Start -```{r, out.lines=-(rocrate_lines(basic_rocrate) - 2)} -basic_rocrate <- o |> - dsROCrate::safe_project(rocrate = basic_rocrate, - project = PROJECT) - -print(basic_rocrate) # note that the output will be truncated -``` - -#### Safe People -To add details for Safe People, use the function `dsROCrate::safe_people()`. - -```{r safe_people, out.lines=-(rocrate_lines(basic_rocrate) + 1)} -basic_rocrate <- o |> - dsROCrate::safe_people(rocrate = basic_rocrate, user = PEOPLE) - -print(basic_rocrate) # note that the output will be truncated -``` - -#### Safe Setting -To add details for Safe Setting, use the function `dsROCrate::safe_setting()`. - -**⚠️NOTE:** The `dsROCrate::safe_setting` function requires administrator -privileges, so here, we will have to log in with administrator credentials (if -you used a non-administrator account previously). - -```{r, eval = FALSE} -# close previous connection -opalr::opal.logout(o) - -# open new connection as administrator -o <- opalr::opal.login( +```{r quick_start, eval = FALSE} +# open connection +con <- opalr::opal.login( username = "administrator", password = "password", - url = SERVER + url = "https://opal-demo.obiba.org" ) -``` -Then, we can proceed as per usual: -```{r, out.lines=-(rocrate_lines(basic_rocrate) - 2)} -basic_rocrate <- o |> - dsROCrate::safe_setting(rocrate = basic_rocrate) +# generate RO-Crate with Five Safes components +crate <- con |> + # initialise RO-Crate + dsROCrate::init(project = "CNSIM", tables = "CNSIM1", user = "dsuser") |> + # extract Five Safes components + dsROCrate::safe_people() |> + dsROCrate::safe_project() |> + dsROCrate::safe_data() |> + dsROCrate::safe_setting() |> + dsROCrate::safe_output() -print(basic_rocrate) # note that the output will be truncated +# generate report +dsROCrate::report(crate, title = "DataSHIELD Five Safes report") ``` -#### Safe Outputs -To add details for Safe Outputs, use the function `dsROCrate::safe_output()`. -Currently, only log files from the operations executed by the user within a -specific period. Set the period using `logs_from` and `logs_to`. Additionally, -a list of functions executed by the user are extracted in a separate file/entity. - -**⚠️NOTE:** Similar to `dsROCrate::safe_setting`, the `dsROCrate::safe_output` -function requires of administrator rights, so here, we will have to log in with -administrator credentials: +Alternatively, the `dsROCrate::audit()` function can be used to generate an audit with +all the Five Safes components: ```{r, eval = FALSE} -# close previous connection -opalr::opal.logout(o) - -# open new connection as administrator -o <- opalr::opal.login( +proj_audit <- opalr::opal.login( username = "administrator", password = "password", - url = SERVER -) -``` - ---------- - -##### DataSHIELD operations - -**⚠️NOTE:** Before extracting logs, ensure there is recent activity on the -server for testing purposes. This can be done using the following commands: - -###### Setup -You will need the following packages: - -```{r, eval = FALSE} -pak::pak("DSI") -pak::pak("DSOpal") -pak::pak("dsBaseClient") -``` - -###### Open connection -```{r} -# run some test commands with dsBaseClient -## needed to defined the OpalDriver class in the current environment -DSOpal::Opal() -## create new login object, note that here we use the `dsuser` -builder <- DSI::newDSLoginBuilder() -builder$append(server = "study1", - url = SERVER, - user = "dsuser", - password = DSUSERPASS, - driver = "OpalDriver") -logindata <- builder$build() -conns <- DSI::datashield.login(logins = logindata) -``` - -###### Simulate some operations -```{r, eval = TRUE} -## assign data -DSI::datashield.assign.table(conns["study1"], - symbol = "dsROCrate_test", - table = paste0(PROJECT, ".", TABLES[1]), - errors.print = TRUE) - -dsBaseClient::ds.ls(datasources = conns["study1"]) -dsBaseClient::ds.summary("dsROCrate_test") -``` - ---------- - -Then, we can proceed as per usual: - -```{r safe_outputs_internal, echo=FALSE} -lines_before_safe_outputs <- rocrate_lines(basic_rocrate) -``` - -```{r safe_outputs} -basic_rocrate <- o |> - dsROCrate::safe_output(rocrate = basic_rocrate, - logs_from = Sys.time() - 60, # capture the last minute - logs_to = Sys.time()) -``` - -```{r, out.lines=-(lines_before_safe_outputs + 1)} -print(basic_rocrate) # note that the output will be truncated -``` - -### 2.4. Close connection - -```{r} -opalr::opal.logout(o) -``` - -### 2.5. Bag/Save RO-Crate - -The resulting RO-Crate can be stored into an RO-Crate bag/archive with the -function `rocrateR::bag_rocrate`: - -```{r} -# create temp directory -dir.create("./rocrates", showWarnings = FALSE) -``` - -NOTE: In the above example, a `path` to store the logs wasn't provided when -calling `dsROCrate::safe_output`, before creating an RO-Crate bag, we should -save the contents of this file first. In addition, the contents for the entity -with the list of functions executed: - -```{r, eval = TRUE, warning = FALSE} -logs_entity <- basic_rocrate |> - rocrateR::get_entity(type = "File") -# write file using the path given by `@id` -## write raw logs -writeLines( - logs_entity[[1]]$content[[1]], - file.path("./rocrates", logs_entity[[1]]$`@id`) -) -## write CSV with mappings and executed functions -write.csv( - logs_entity[[2]]$content[[1]], - file.path("./rocrates", logs_entity[[2]]$`@id`), - row.names = FALSE -) - -# remove the section `content` -logs_entity[[1]]$content <- NULL -logs_entity[[2]]$content <- NULL -# update the RO-Crate -basic_rocrate <- basic_rocrate |> - rocrateR::add_entity(logs_entity[[1]], overwrite = TRUE) |> - rocrateR::add_entity(logs_entity[[2]], overwrite = TRUE) -``` - -```{r} -# create RO-Crate bag -path_to_rocrate_bag <- basic_rocrate |> - rocrateR::bag_rocrate(path = "./rocrates", overwrite = TRUE) -``` - -We can explore the contents with the following commands: - -```{r} -# extract files in temporary directory -path_to_rocrate_bag |> - # extract contents inside ./rocrates/ROC - rocrateR::unbag_rocrate(output = "./rocrates/ROC", quiet = TRUE) |> - # create tree with the files - fs::dir_tree() -``` - -### 2.6. Clean working directory - -```{r} -unlink("./rocrates", recursive = TRUE, force = TRUE) -``` - - -
- -## 3. Auditing RO-Crates and servers - -### 3.1. Audit People - -##### List accessible tables within a project for an user -```{r, warning=FALSE} -safe_people_crate_v1 <- opalr::opal.login( - username = USERNAME, - password = USERPASS, - url = SERVER + url = "https://opal-demo.obiba.org" ) |> - dsROCrate::audit_safe_people(user = "dsuser", project = "CNSIM") + dsROCrate::audit(project = "CNSIM", tables = "CNSIM1", user = "dsuser") -print(safe_people_crate_v1) +# generate report +dsROCrate::report(proj_audit, title = "DataSHIELD Safe People - Audit Report") ``` -###### Markdown report +## Learn More -A markdown report can be created with an overview and details for an RO-Crate, -using the `dsROCrate::rocrate_report`: - -**Only generate .Rmd file** - -```{r safe_people_crate_audit_v1} -safe_people_crate_v1_rmd <- tempfile(fileext = ".Rmd") # temporary file - -safe_people_crate_contents <- safe_people_crate_v1 |> - dsROCrate::rocrate_report(filepath = safe_people_crate_v1_rmd, render = FALSE) - -# display Overview diagram -safe_people_crate_contents$overview_diagram - -# display Overview data (Safe People, Safe Projects and Safe Data) -safe_people_crate_contents$overview_data |> - knitr::kable() -``` - -**Render and display report (HTML)** +For further details, see the following vignette: ```{r, eval = FALSE} -safe_people_crate_v1 |> - dsROCrate::rocrate_report(filepath = safe_people_crate_v1_rmd, - title = "DataSHIELD Safe People - Audit Report", - render = TRUE, - overwrite = TRUE) -``` - -##### List all accessible projects & tables for an user -```{r safe_people_crate_audit_v2, warning=FALSE} -safe_people_crate_v2 <- opalr::opal.login( - username = USERNAME, - password = USERPASS, - url = SERVER -) |> - dsROCrate::audit_safe_people(user = "dsuser") - -safe_people_crate_v2_rmd <- tempfile(fileext = ".Rmd") # temporary file - -safe_people_crate_contents_v2 <- safe_people_crate_v2 |> - dsROCrate::rocrate_report(filepath = safe_people_crate_v2_rmd, render = FALSE) - -# display Overview diagram -safe_people_crate_contents_v2$overview_diagram - -# display Overview data (Safe People, Safe Projects and Safe Data) -safe_people_crate_contents_v2$overview_data |> - knitr::kable() +vignette("getting-started", package = "dsROCrate") ``` -### 3.2. Audit Project - -##### List users and dataset/table level permissions within a project -```{r, warning=FALSE} -safe_project_crate_v1 <- opalr::opal.login( - username = USERNAME, - password = USERPASS, - url = SERVER -) |> - dsROCrate::audit_safe_project(project = "CNSIM") - -print(safe_project_crate_v1) -``` - -###### Markdown report - -A markdown report can be created with an overview and details for an RO-Crate, -using the `dsROCrate::rocrate_report`: - -**Only generate .Rmd file** - -```{r safe_project_crate_audit_v1} -safe_project_crate_v1_rmd <- tempfile(fileext = ".Rmd") # temporary file - -safe_project_crate_contents <- safe_project_crate_v1 |> - dsROCrate::rocrate_report(filepath = safe_project_crate_v1_rmd, render = FALSE) - -# display Overview diagram -safe_project_crate_contents$overview_diagram - -# display Overview data (Safe People, Safe Projects and Safe Data) -safe_project_crate_contents$overview_data |> - knitr::kable() -``` - -**Render and display report (HTML)** - -```{r, eval = FALSE} -safe_project_crate_v1 |> - dsROCrate::rocrate_report(filepath = safe_project_crate_v1_rmd, - title = "DataSHIELD Safe Project - Audit Report", - render = TRUE, - overwrite = TRUE) -``` - -
- -### 3.3. Audit Study - -##### List users and dataset/table level permissions within a study (i.e., multiple servers) -```{r, warning=FALSE} -study_crate_v1 <- - list( - "opal_test" = opalr::opal.login( - username = USERNAME, - password = USERPASS, - url = "https://opal-test.obiba.org" - ), - "opal_demo" = opalr::opal.login( - username = USERNAME, - password = USERPASS, - url = "https://opal-demo.obiba.org" - ) - ) |> - dsROCrate::audit_study(project = "CNSIM") - -print(study_crate_v1) -``` - -###### Markdown report - -A markdown report can be created with an overview and details for an RO-Crate, -using the `dsROCrate::rocrate_report`: - -**Only generate .Rmd file** - -```{r study_crate_audit_v1} -study_crate_v1_rmd <- tempfile(fileext = ".Rmd") # temporary file - -safe_project_crate_contents <- study_crate_v1 |> - dsROCrate::rocrate_report(filepath = study_crate_v1_rmd, render = FALSE) - -# display Overview diagram -safe_project_crate_contents$overview_diagram - -# display Overview data (Safe People, Safe Projects and Safe Data) -safe_project_crate_contents$overview_data |> - knitr::kable() -``` - -**Render and display report (HTML)** - -```{r, eval = FALSE} -study_crate_v1 |> - dsROCrate::rocrate_report(filepath = study_crate_v1_rmd, - title = "DataSHIELD Study audit", - render = TRUE, - overwrite = TRUE) -``` - -
- -## 4. Identity +## Identity You are welcome to use any of the following hex codes when referencing `{dsROCrate}`: [logo](man/figures/logo.png) [logo-white](man/figures/logo_white.png) [logo-black](man/figures/logo_black.png) - -```{r, echo = FALSE, message = FALSE, error = FALSE} -unlink(safe_people_crate_v1_rmd, TRUE, TRUE) -unlink(safe_people_crate_v2_rmd, TRUE, TRUE) -unlink(safe_project_crate_v1_rmd, TRUE, TRUE) -unlink(study_crate_v1_rmd, TRUE, TRUE) -``` diff --git a/README.md b/README.md index 3e582d5..ce66b20 100644 --- a/README.md +++ b/README.md @@ -1,2810 +1,98 @@ -# dsROCrate: ‘DataSHIELD’ RO-Crate Wrapper Functions logo +# dsROCrate: ‘DataSHIELD’ RO-Crate Governance Functions logo -[![Lifecycle: -experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental) [![CRAN status](https://www.r-pkg.org/badges/version/dsROCrate)](https://CRAN.R-project.org/package=dsROCrate) -[![Codecov test -coverage](https://codecov.io/gh/DataSHIELD-5S/dsROCrate/graph/badge.svg)](https://app.codecov.io/gh/DataSHIELD-5S/dsROCrate) +[![R-CMD-check](https://github.com/FederatedMethods/dsROCrate/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/FederatedMethods/dsROCrate/actions/workflows/R-CMD-check.yaml) + +[![Lifecycle: +experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental) -The goal of dsROCrate is to provide functions to wrap elements from a -‘DataSHIELD’ analysis into an RO-Crate. - -## 1. Installation - -You can install the development version of dsROCrate from -[GitHub](https://github.com/FederatedMethods/dsROCrate/) with: - -``` r -# install.packages("pak") -pak::pak("FederatedMethods/dsROCrate") -``` - -## 2. Creating your first RO-Crate - -In this example, we will be using OBiBa’s -[Opal](https://opaldoc.obiba.org/en/latest/index.html) as the *back-end* -for DataSHIELD. Another option would be MOLGENIS’ -[Armadillo](https://github.com/molgenis/molgenis-service-armadillo/). +## Overview -### 2.1. Connect to an Opal Server +`{dsROCrate}` provides tools to capture, structure and audit DataSHIELD +analyses using the RO-Crate standard. It enables reproducible governance +of federated analyses by packaging inputs, outputs, metadata, and audit +information into a consistent, portable format. -#### Setup +## Key Features -For instructions on how to set up a local server for DataSHIELD, see the -following vignette: +- Create RO-Crates for DataSHIELD workflows +- Capture project metadata, data references and analysis outputs +- Record and audit disclosure control processes +- Support reproducibility and governance in federated environments -``` r -vignette("deploy-local-datashield-server-with-opal") -``` +## Installation -Here we will use OBiBa’s Opal demo server: - which can be accessed with the following -login credentials: +You can install the released version of `{dsROCrate}` from +[CRAN](https://cran.r-project.org/package=dsROCrate) with: ``` r -# define global variables -## Opal server access -USERNAME <- "administrator" -USERPASS <- "password" -SERVER <- "https://opal-demo.obiba.org" -## Credentials for `dsuser` -### NOTE: this is only used to simulate an analysis and generate logs -DSUSERPASS <- "P@ssw0rd" -``` - -Next, define global variables used in generating the RO-Crate, such as -project name, table references (within the project) and user -identifiers. - -``` r -## Five safes variables -PEOPLE <- "dsuser" -PROJECT <- "CNSIM" -TABLES <- c("CNSIM1") -``` - -#### Open connection - -Once the credentials and Five Safes variables are configured,we can -start a new session on the opal server with the following command: - -``` r -# login to local server with `USERNAME` and `USERPASS`. -o <- opalr::opal.login( - username = USERNAME, - password = USERPASS, - url = SERVER -) - -print(o) -#> url: https://opal-demo.obiba.org -#> name: opal-demo.obiba.org -#> version: 5.5.2 -#> username: administrator +install.packages("dsROCrate") ``` -### 2.2. Create a basic RO-Crate - -To create a basic RO-Crate, we will use the -[`{rocrateR}`](https://github.com/ResearchObject/ro-crate-r) package. -This package can be installed with the following command: +And the development version from +[GitHub](https://github.com/FederatedMethods/dsROCrate/) with: ``` r # install.packages("pak") -pak::pak("rocrateR") +pak::pak("FederatedMethods/dsROCrate@dev") ``` -Then, a basic RO-Crate can be created with the following command: +## Quick Start ``` r -basic_rocrate <- rocrateR::rocrate_5s() -``` - -Note that this RO-Crate uses the -[5s-crate](https://trefx.uk/5s-crate/0.4/) profile. - -``` r -print(basic_rocrate) -#> { -#> "@context": "https://w3id.org/ro/crate/1.2/context", -#> "@graph": [ -#> { -#> "@id": "ro-crate-metadata.json", -#> "@type": "CreativeWork", -#> "about": { -#> "@id": "./" -#> }, -#> "conformsTo": { -#> "@id": "https://w3id.org/ro/crate/1.2" -#> } -#> }, -#> { -#> "@id": "./", -#> "@type": "Dataset", -#> "name": "", -#> "description": "", -#> "datePublished": "2026-02-27", -#> "license": { -#> "@id": "http://spdx.org/licenses/CC-BY-4.0" -#> }, -#> "conformsTo": { -#> "@id": "https://w3id.org/5s-crate/0.4" -#> } -#> }, -#> { -#> "@id": "https://w3id.org/5s-crate/0.4", -#> "@type": ["CreativeWork", "Profile"], -#> "name": "Five Safes RO-Crate profile" -#> } -#> ] -#> } -``` - -### 2.3. Add the *Five Safes* Elements - -#### Safe Data - -To add details for Safe Data, use the function `dsROCrate::safe_data()`. - -``` r -basic_rocrate <- o |> - dsROCrate::safe_data(rocrate = basic_rocrate, - project = PROJECT, - tables = TABLES) - -print(basic_rocrate) # note that the output will be truncated -... -#> { -#> "@id": "#dataset:67adf2d8e106aca9b11de773758bd241", -#> "@type": "Dataset", -#> "name": "CNSIM1", -#> "dateCreated": "2026-02-27T06:29:50.731Z", -#> "dateModified": "2026-02-27T06:29:51.865Z", -#> "path": "/datasource/CNSIM/table/CNSIM1" -#> } -#> ] -#> } -``` - -#### Safe Project - -To add details for Safe Project, use the function -`dsROCrate::safe_project()`. - -``` r -basic_rocrate <- o |> - dsROCrate::safe_project(rocrate = basic_rocrate, - project = PROJECT) - -print(basic_rocrate) # note that the output will be truncated -... -#> { -#> "@id": "#project:7ba189863f9f641196596cb28e04aa14", -#> "@type": "Project", -#> "name": "CNSIM", -#> "dateCreated": "2026-02-27T06:29:49.235Z", -#> "dateModified": "2026-02-27T06:29:54.200Z", -#> "hasPart": [ -#> { -#> "@id": "#dataset:67adf2d8e106aca9b11de773758bd241" -#> } -#> ] -#> } -#> ] -#> } -``` - -#### Safe People - -To add details for Safe People, use the function -`dsROCrate::safe_people()`. - -``` r -basic_rocrate <- o |> - dsROCrate::safe_people(rocrate = basic_rocrate, user = PEOPLE) - -print(basic_rocrate) # note that the output will be truncated -... -#> { -#> "@id": "#person:a0af2a94926db1b49ad7a812eef509d2", -#> "@type": "Person", -#> "name": "dsuser", -#> "memberOf": [ -#> { -#> "@id": "#project:7ba189863f9f641196596cb28e04aa14" -#> } -#> ] -#> } -#> ] -#> } -``` - -#### Safe Setting - -To add details for Safe Setting, use the function -`dsROCrate::safe_setting()`. - -**⚠️NOTE:** The `dsROCrate::safe_setting` function requires -administrator privileges, so here, we will have to log in with -administrator credentials (if you used a non-administrator account -previously). - -``` r -# close previous connection -opalr::opal.logout(o) - -# open new connection as administrator -o <- opalr::opal.login( +# open connection +con <- opalr::opal.login( username = "administrator", password = "password", - url = SERVER + url = "https://opal-demo.obiba.org" ) -``` - -Then, we can proceed as per usual: -``` r -basic_rocrate <- o |> - dsROCrate::safe_setting(rocrate = basic_rocrate) +# generate RO-Crate with Five Safes components +crate <- con |> + # initialise RO-Crate + dsROCrate::init(project = "CNSIM", tables = "CNSIM1", user = "dsuser") |> + # extract Five Safes components + dsROCrate::safe_people() |> + dsROCrate::safe_project() |> + dsROCrate::safe_data() |> + dsROCrate::safe_setting() |> + dsROCrate::safe_output() -print(basic_rocrate) # note that the output will be truncated -... -#> { -#> "@id": "_:localid:datashield.privacyLevel:5", -#> "@type": "PropertyValue", -#> "name": "datashield.privacyLevel", -#> "value": "5" -#> }, -#> { -#> "@id": "_:localid:default.datashield.privacyControlLevel:banana", -#> "@type": "PropertyValue", -#> "name": "default.datashield.privacyControlLevel", -#> "value": "banana" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.glm:0.33", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.glm", -#> "value": "0.33" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.kNN:3", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.kNN", -#> "value": "3" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.levels.density:0.33", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.levels.density", -#> "value": "0.33" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.levels.max:40", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.levels.max", -#> "value": "40" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.noise:0.25", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.noise", -#> "value": "0.25" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.string:80", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.string", -#> "value": "80" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.stringShort:20", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.stringShort", -#> "value": "20" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.subset:3", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.subset", -#> "value": "3" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.tab:3", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.tab", -#> "value": "3" -#> }, -#> { -#> "@id": "_:localid:3aaeab3631b4c9bbe7f44d60805b8f9c", -#> "@type": "SoftwareApplication", -#> "name": "dsBase", -#> "version": "6.3.5", -#> "description": "Base 'DataSHIELD' functions for the server side. 'DataSHIELD' is a software package which allows\n you to do non-disclosive federated analysis on sensitive data. 'DataSHIELD' analytic functions have\n been designed to only share non disclosive summary statistics, with built in automated output\n checking based on statistical disclosure control. With data sites setting the threshold values for\n the automated output checks. For more details, see 'citation(\"dsBase\")'." -#> }, -#> { -#> "@id": "_:localid:e2f7c43973c40d7a6a6731da5a0aa564", -#> "@type": "SoftwareApplication", -#> "name": "dsTidyverse", -#> "version": "1.1.0", -#> "description": "Implementation of selected 'Tidyverse' functions within 'DataSHIELD', an open-source federated analysis solution in R. Currently, DataSHIELD contains very limited tools for data manipulation, so the aim of this package is to improve the researcher experience by implementing essential functions for data manipulation, including subsetting, filtering, grouping, and renaming variables. This is the serverside package which should be installed on the server holding the data, and is used in conjuncture with the clientside package 'dsTidyverseClient' which is installed in the local R environment of the analyst. For more information, see and ." -#> }, -#> { -#> "@id": "_:localid:411453e2513e6d909fe2cb8273b034dc", -#> "@type": "SoftwareApplication", -#> "name": "resourcer", -#> "version": "1.5.0", -#> "description": "A resource represents some data or a computation unit. It is \n described by a URL and credentials. This package proposes a Resource model\n with \"resolver\" and \"client\" classes to facilitate the access and the usage of the \n resources." -#> } -#> ] -#> } +# generate report +dsROCrate::report(crate, title = "DataSHIELD Five Safes report") ``` -#### Safe Outputs - -To add details for Safe Outputs, use the function -`dsROCrate::safe_output()`. Currently, only log files from the -operations executed by the user within a specific period. Set the period -using `logs_from` and `logs_to`. Additionally, a list of functions -executed by the user are extracted in a separate file/entity. - -**⚠️NOTE:** Similar to `dsROCrate::safe_setting`, the -`dsROCrate::safe_output` function requires of administrator rights, so -here, we will have to log in with administrator credentials: +Alternatively, the `dsROCrate::audit()` function can be used to generate +an audit with all the Five Safes components: ``` r -# close previous connection -opalr::opal.logout(o) - -# open new connection as administrator -o <- opalr::opal.login( +proj_audit <- opalr::opal.login( username = "administrator", password = "password", - url = SERVER -) -``` - ------------------------------------------------------------------------- - -##### DataSHIELD operations - -**⚠️NOTE:** Before extracting logs, ensure there is recent activity on -the server for testing purposes. This can be done using the following -commands: - -###### Setup - -You will need the following packages: - -``` r -pak::pak("DSI") -pak::pak("DSOpal") -pak::pak("dsBaseClient") -``` - -###### Open connection - -``` r -# run some test commands with dsBaseClient -## needed to defined the OpalDriver class in the current environment -DSOpal::Opal() -#> An object of class "OpalDriver" -#> -## create new login object, note that here we use the `dsuser` -builder <- DSI::newDSLoginBuilder() -builder$append(server = "study1", - url = SERVER, - user = "dsuser", - password = DSUSERPASS, - driver = "OpalDriver") -logindata <- builder$build() -conns <- DSI::datashield.login(logins = logindata) -#> -#> Logging into the collaborating servers -``` - -###### Simulate some operations - -``` r -## assign data -DSI::datashield.assign.table(conns["study1"], - symbol = "dsROCrate_test", - table = paste0(PROJECT, ".", TABLES[1]), - errors.print = TRUE) - -dsBaseClient::ds.ls(datasources = conns["study1"]) -#> $study1 -#> $study1$environment.searched -#> [1] "R_GlobalEnv" -#> -#> $study1$objects.found -#> [1] "dsROCrate_test" -dsBaseClient::ds.summary("dsROCrate_test") -#> $study1 -#> $study1$class -#> [1] "data.frame" -#> -#> $study1$`number of rows` -#> [1] 2163 -#> -#> $study1$`number of columns` -#> [1] 11 -#> -#> $study1$`variables held` -#> [1] "LAB_TSC" "LAB_TRIG" "LAB_HDL" -#> [4] "LAB_GLUC_ADJUSTED" "PM_BMI_CONTINUOUS" "DIS_CVA" -#> [7] "MEDI_LPD" "DIS_DIAB" "DIS_AMI" -#> [10] "GENDER" "PM_BMI_CATEGORICAL" -``` - ------------------------------------------------------------------------- - -Then, we can proceed as per usual: - -``` r -basic_rocrate <- o |> - dsROCrate::safe_output(rocrate = basic_rocrate, - logs_from = Sys.time() - 60, # capture the last minute - logs_to = Sys.time()) -#> opening file input connection. -#> Found 83 records... Imported 83 records. Simplifying... -#> closing file input connection. -#> Warning: A `path` wasn't provided! The logs will be included in the RO-Crate -#> object, under the `content` tag! -``` - -``` r -print(basic_rocrate) # note that the output will be truncated -... -#> "@type": "SoftwareApplication", -#> "name": "resourcer", -#> "version": "1.5.0", -#> "description": "A resource represents some data or a computation unit. It is \n described by a URL and credentials. This package proposes a Resource model\n with \"resolver\" and \"client\" classes to facilitate the access and the usage of the \n resources." -#> }, -#> { -#> "@id": "20260227T152405-dslogs-dsuser.log", -#> "@type": "File", -#> "dateModified": "2026-02-27 15:24:05", -#> "name": "20260227T152405-dslogs-dsuser.log", -#> "description": "This file contains the raw logs for the user: `dsuser` , between: 2026-02-27 15:23:05 and 2026-02-27 15:24:05", -#> "encodingFormat": "text/plain", -#> "content": [ -#> ["[INFO][2026-02-27T15:24:02][OPEN] created a datashield session fc258313-6846-4a8e-93d9-4809cd794fbe", "[INFO][2026-02-27T15:24:02][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T15:24:03][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:24:04][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:24:04][PARSE] parsed 'base::exists(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][AGGREGATE] evaluated 'base::exists(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][PARSE] parsed 'dsBase::classDS(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][AGGREGATE] evaluated 'dsBase::classDS(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][PARSE] parsed 'dsBase::isValidDS(dsROCrate_test)'", "[INFO][2026-02-27T15:24:04][AGGREGATE] evaluated 'dsBase::isValidDS(dsROCrate_test)'", "[INFO][2026-02-27T15:24:04][PARSE] parsed 'dsBase::dimDS(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][AGGREGATE] evaluated 'dsBase::dimDS(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][PARSE] parsed 'dsBase::colnamesDS(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:05][AGGREGATE] evaluated 'dsBase::colnamesDS(\"dsROCrate_test\")'"] -#> ] -#> }, -#> { -#> "@id": "20260227T152405-dslogs-dsuser_mappings.csv", -#> "@type": "File", -#> "dateModified": "2026-02-27 15:24:05", -#> "name": "20260227T152405-dslogs-dsuser_mappings.csv", -#> "description": "This file contains mappings and evaluated functions", -#> "encodingFormat": "text/csv", -#> "content": [ -#> [ -#> { -#> "timestamp": "2026-02-27T15:24:02", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "fx": "DSI::datashield.login", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:02", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:04", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:04", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "base::exists(\"dsROCrate_test\")", -#> "fx": "base::exists", -#> "symbol": "dsROCrate_test", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:04", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::classDS(\"dsROCrate_test\")", -#> "fx": "dsBase::classDS", -#> "symbol": "dsROCrate_test", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:04", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::isValidDS(dsROCrate_test)", -#> "fx": "dsBase::isValidDS", -#> "symbol": "dsROCrate_test", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:04", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::dimDS(\"dsROCrate_test\")", -#> "fx": "dsBase::dimDS", -#> "symbol": "dsROCrate_test", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:05", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::colnamesDS(\"dsROCrate_test\")", -#> "fx": "dsBase::colnamesDS", -#> "symbol": "dsROCrate_test", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> } -#> ] -#> ] -#> } -#> ] -#> } -``` - -### 2.4. Close connection - -``` r -opalr::opal.logout(o) -``` - -### 2.5. Bag/Save RO-Crate - -The resulting RO-Crate can be stored into an RO-Crate bag/archive with -the function `rocrateR::bag_rocrate`: - -``` r -# create temp directory -dir.create("./rocrates", showWarnings = FALSE) -``` - -NOTE: In the above example, a `path` to store the logs wasn’t provided -when calling `dsROCrate::safe_output`, before creating an RO-Crate bag, -we should save the contents of this file first. In addition, the -contents for the entity with the list of functions executed: - -``` r -logs_entity <- basic_rocrate |> - rocrateR::get_entity(type = "File") -# write file using the path given by `@id` -## write raw logs -writeLines( - logs_entity[[1]]$content[[1]], - file.path("./rocrates", logs_entity[[1]]$`@id`) -) -## write CSV with mappings and executed functions -write.csv( - logs_entity[[2]]$content[[1]], - file.path("./rocrates", logs_entity[[2]]$`@id`), - row.names = FALSE -) - -# remove the section `content` -logs_entity[[1]]$content <- NULL -logs_entity[[2]]$content <- NULL -# update the RO-Crate -basic_rocrate <- basic_rocrate |> - rocrateR::add_entity(logs_entity[[1]], overwrite = TRUE) |> - rocrateR::add_entity(logs_entity[[2]], overwrite = TRUE) -``` - -``` r -# create RO-Crate bag -path_to_rocrate_bag <- basic_rocrate |> - rocrateR::bag_rocrate(path = "./rocrates", overwrite = TRUE) -#> RO-Crate successfully 'bagged'! -#> For details, see: ./rocrates/rocrate-200ecad114285e36b84c12cfa3be6526.zip -``` - -We can explore the contents with the following commands: - -``` r -# extract files in temporary directory -path_to_rocrate_bag |> - # extract contents inside ./rocrates/ROC - rocrateR::unbag_rocrate(output = "./rocrates/ROC", quiet = TRUE) |> - # create tree with the files - fs::dir_tree() -#> ./rocrates/ROC/ -#> ├── bagit.txt -#> ├── data -#> │ ├── 20260227T152405-dslogs-dsuser.log -#> │ ├── 20260227T152405-dslogs-dsuser_mappings.csv -#> │ └── ro-crate-metadata.json -#> ├── manifest-sha512.txt -#> └── tagmanifest-sha512.txt -``` - -### 2.6. Clean working directory - -``` r -unlink("./rocrates", recursive = TRUE, force = TRUE) -``` - -
- -## 3. Auditing RO-Crates and servers - -### 3.1. Audit People - -##### List accessible tables within a project for an user - -``` r -safe_people_crate_v1 <- opalr::opal.login( - username = USERNAME, - password = USERPASS, - url = SERVER + url = "https://opal-demo.obiba.org" ) |> - dsROCrate::audit_safe_people(user = "dsuser", project = "CNSIM") -#> opening file input connection. -#> Found 83 records... Imported 83 records. Simplifying... -#> closing file input connection. + dsROCrate::audit(project = "CNSIM", tables = "CNSIM1", user = "dsuser") -print(safe_people_crate_v1) -#> { -#> "@context": "https://w3id.org/ro/crate/1.2/context", -#> "@graph": [ -#> { -#> "@id": "ro-crate-metadata.json", -#> "@type": "CreativeWork", -#> "about": { -#> "@id": "./" -#> }, -#> "conformsTo": { -#> "@id": "https://w3id.org/ro/crate/1.2" -#> } -#> }, -#> { -#> "@id": "./", -#> "@type": "Dataset", -#> "name": "", -#> "description": "", -#> "datePublished": "2026-02-27", -#> "license": { -#> "@id": "http://spdx.org/licenses/CC-BY-4.0" -#> }, -#> "conformsTo": { -#> "@id": "https://w3id.org/5s-crate/0.4" -#> }, -#> "hasPart": [ -#> { -#> "@id": "20260227T152406-dslogs-dsuser.log" -#> }, -#> { -#> "@id": "20260227T152406-dslogs-dsuser_mappings.csv" -#> } -#> ] -#> }, -#> { -#> "@id": "https://w3id.org/5s-crate/0.4", -#> "@type": ["CreativeWork", "Profile"], -#> "name": "Five Safes RO-Crate profile" -#> }, -#> { -#> "@id": "#dataset:67adf2d8e106aca9b11de773758bd241", -#> "@type": "Dataset", -#> "name": "CNSIM1", -#> "dateCreated": "2026-02-27T06:29:50.731Z", -#> "dateModified": "2026-02-27T06:29:51.865Z", -#> "path": "/datasource/CNSIM/table/CNSIM1" -#> }, -#> { -#> "@id": "#dataset:ffb1b1adcafc024743be1b0c252787c9", -#> "@type": "Dataset", -#> "name": "CNSIM2", -#> "dateCreated": "2026-02-27T06:29:51.872Z", -#> "dateModified": "2026-02-27T06:29:53.057Z", -#> "path": "/datasource/CNSIM/table/CNSIM2" -#> }, -#> { -#> "@id": "#dataset:cc3061aef69ce457358815fb9d8c6492", -#> "@type": "Dataset", -#> "name": "CNSIM3", -#> "dateCreated": "2026-02-27T06:29:53.064Z", -#> "dateModified": "2026-02-27T06:29:54.200Z", -#> "path": "/datasource/CNSIM/table/CNSIM3" -#> }, -#> { -#> "@id": "#project:7ba189863f9f641196596cb28e04aa14", -#> "@type": "Project", -#> "name": "CNSIM", -#> "dateCreated": "2026-02-27T06:29:49.235Z", -#> "dateModified": "2026-02-27T06:29:54.200Z", -#> "hasPart": [ -#> { -#> "@id": "#dataset:67adf2d8e106aca9b11de773758bd241" -#> }, -#> { -#> "@id": "#dataset:ffb1b1adcafc024743be1b0c252787c9" -#> }, -#> { -#> "@id": "#dataset:cc3061aef69ce457358815fb9d8c6492" -#> } -#> ] -#> }, -#> { -#> "@id": "#person:a0af2a94926db1b49ad7a812eef509d2", -#> "@type": "Person", -#> "name": "dsuser" -#> }, -#> { -#> "@id": "#perm:9bf7f75b6c5b07d02830b95652cd39a0-dict-summary-read", -#> "@type": "ReadAction", -#> "agent": { -#> "@id": "#person:a0af2a94926db1b49ad7a812eef509d2" -#> }, -#> "object": { -#> "@id": "#dataset:67adf2d8e106aca9b11de773758bd241" -#> }, -#> "actionStatus": "PotentialActionStatus", -#> "description": "User may view table dictionary and summary statistics only; access to individual values is restricted." -#> }, -#> { -#> "@id": "#perm:363eb627d1e49c08933f2e26142e6d56-dict-summary-read", -#> "@type": "ReadAction", -#> "agent": { -#> "@id": "#person:a0af2a94926db1b49ad7a812eef509d2" -#> }, -#> "object": { -#> "@id": "#dataset:ffb1b1adcafc024743be1b0c252787c9" -#> }, -#> "actionStatus": "PotentialActionStatus", -#> "description": "User may view table dictionary and summary statistics only; access to individual values is restricted." -#> }, -#> { -#> "@id": "#perm:63b8097908f682bff1760e48d28c5855-dict-summary-read", -#> "@type": "ReadAction", -#> "agent": { -#> "@id": "#person:a0af2a94926db1b49ad7a812eef509d2" -#> }, -#> "object": { -#> "@id": "#dataset:cc3061aef69ce457358815fb9d8c6492" -#> }, -#> "actionStatus": "PotentialActionStatus", -#> "description": "User may view table dictionary and summary statistics only; access to individual values is restricted." -#> }, -#> { -#> "@id": "_:localid:datashield.privacyLevel:5", -#> "@type": "PropertyValue", -#> "name": "datashield.privacyLevel", -#> "value": "5" -#> }, -#> { -#> "@id": "_:localid:default.datashield.privacyControlLevel:banana", -#> "@type": "PropertyValue", -#> "name": "default.datashield.privacyControlLevel", -#> "value": "banana" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.glm:0.33", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.glm", -#> "value": "0.33" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.kNN:3", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.kNN", -#> "value": "3" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.levels.density:0.33", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.levels.density", -#> "value": "0.33" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.levels.max:40", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.levels.max", -#> "value": "40" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.noise:0.25", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.noise", -#> "value": "0.25" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.string:80", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.string", -#> "value": "80" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.stringShort:20", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.stringShort", -#> "value": "20" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.subset:3", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.subset", -#> "value": "3" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.tab:3", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.tab", -#> "value": "3" -#> }, -#> { -#> "@id": "_:localid:3aaeab3631b4c9bbe7f44d60805b8f9c", -#> "@type": "SoftwareApplication", -#> "name": "dsBase", -#> "version": "6.3.5", -#> "description": "Base 'DataSHIELD' functions for the server side. 'DataSHIELD' is a software package which allows\n you to do non-disclosive federated analysis on sensitive data. 'DataSHIELD' analytic functions have\n been designed to only share non disclosive summary statistics, with built in automated output\n checking based on statistical disclosure control. With data sites setting the threshold values for\n the automated output checks. For more details, see 'citation(\"dsBase\")'." -#> }, -#> { -#> "@id": "_:localid:e2f7c43973c40d7a6a6731da5a0aa564", -#> "@type": "SoftwareApplication", -#> "name": "dsTidyverse", -#> "version": "1.1.0", -#> "description": "Implementation of selected 'Tidyverse' functions within 'DataSHIELD', an open-source federated analysis solution in R. Currently, DataSHIELD contains very limited tools for data manipulation, so the aim of this package is to improve the researcher experience by implementing essential functions for data manipulation, including subsetting, filtering, grouping, and renaming variables. This is the serverside package which should be installed on the server holding the data, and is used in conjuncture with the clientside package 'dsTidyverseClient' which is installed in the local R environment of the analyst. For more information, see and ." -#> }, -#> { -#> "@id": "_:localid:411453e2513e6d909fe2cb8273b034dc", -#> "@type": "SoftwareApplication", -#> "name": "resourcer", -#> "version": "1.5.0", -#> "description": "A resource represents some data or a computation unit. It is \n described by a URL and credentials. This package proposes a Resource model\n with \"resolver\" and \"client\" classes to facilitate the access and the usage of the \n resources." -#> }, -#> { -#> "@id": "20260227T152406-dslogs-dsuser.log", -#> "@type": "File", -#> "dateModified": "2026-02-27 15:24:06", -#> "name": "20260227T152406-dslogs-dsuser.log", -#> "description": "This file contains the raw logs for the user: `dsuser` , between: ALL and ALL", -#> "encodingFormat": "text/plain", -#> "content": [ -#> ["[INFO][2026-02-27T08:56:24][OPEN] created a datashield session 622f2c74-9242-48e2-82d9-bc3250f8aa4b", "[INFO][2026-02-27T08:56:26][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T08:56:26][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T08:56:27][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T09:00:37][OPEN] created a datashield session 7925f5e8-f7b6-416e-b05b-5ac2c19f00c1", "[INFO][2026-02-27T09:00:37][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T09:00:38][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T09:00:38][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T09:20:47][OPEN] created a datashield session 08413766-4fa8-4eec-9392-ec30581fb48c", "[INFO][2026-02-27T09:20:48][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T09:20:48][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T09:20:49][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T14:08:39][OPEN] created a datashield session 88a48432-61c8-4444-bb6f-1d0174d4f177", "[INFO][2026-02-27T14:08:39][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T14:08:39][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T14:08:40][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T14:18:40][OPEN] created a datashield session e45b3573-21cc-445d-8d22-b7fc289d279a", "[INFO][2026-02-27T14:18:40][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T14:18:40][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T14:18:41][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:04:13][OPEN] created a datashield session 026b4b18-c40c-4f08-8a19-75716fee1c75", "[INFO][2026-02-27T15:04:13][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T15:04:14][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:04:14][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:13:01][OPEN] created a datashield session a3a6f257-79ef-443c-a110-393b1ff196f0", "[INFO][2026-02-27T15:13:01][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T15:13:02][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:13:03][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:24:02][OPEN] created a datashield session fc258313-6846-4a8e-93d9-4809cd794fbe", "[INFO][2026-02-27T15:24:02][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T15:24:03][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:24:04][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:24:04][PARSE] parsed 'base::exists(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][AGGREGATE] evaluated 'base::exists(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][PARSE] parsed 'dsBase::classDS(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][AGGREGATE] evaluated 'dsBase::classDS(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][PARSE] parsed 'dsBase::isValidDS(dsROCrate_test)'", "[INFO][2026-02-27T15:24:04][AGGREGATE] evaluated 'dsBase::isValidDS(dsROCrate_test)'", "[INFO][2026-02-27T15:24:04][PARSE] parsed 'dsBase::dimDS(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][AGGREGATE] evaluated 'dsBase::dimDS(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][PARSE] parsed 'dsBase::colnamesDS(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:05][AGGREGATE] evaluated 'dsBase::colnamesDS(\"dsROCrate_test\")'"] -#> ] -#> }, -#> { -#> "@id": "20260227T152406-dslogs-dsuser_mappings.csv", -#> "@type": "File", -#> "dateModified": "2026-02-27 15:24:06", -#> "name": "20260227T152406-dslogs-dsuser_mappings.csv", -#> "description": "This file contains mappings and evaluated functions", -#> "encodingFormat": "text/csv", -#> "content": [ -#> [ -#> { -#> "timestamp": "2026-02-27T08:56:24", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: 622f2c74-9242-48e2-82d9-bc3250f8aa4b", -#> "fx": "DSI::datashield.login", -#> "session": "622f2c74-9242-48e2-82d9-bc3250f8aa4b", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T08:56:26", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "622f2c74-9242-48e2-82d9-bc3250f8aa4b", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T08:56:27", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "622f2c74-9242-48e2-82d9-bc3250f8aa4b", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T09:00:37", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: 7925f5e8-f7b6-416e-b05b-5ac2c19f00c1", -#> "fx": "DSI::datashield.login", -#> "session": "7925f5e8-f7b6-416e-b05b-5ac2c19f00c1", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T09:00:37", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "7925f5e8-f7b6-416e-b05b-5ac2c19f00c1", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T09:00:38", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "7925f5e8-f7b6-416e-b05b-5ac2c19f00c1", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T09:20:47", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: 08413766-4fa8-4eec-9392-ec30581fb48c", -#> "fx": "DSI::datashield.login", -#> "session": "08413766-4fa8-4eec-9392-ec30581fb48c", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T09:20:48", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "08413766-4fa8-4eec-9392-ec30581fb48c", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T09:20:49", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "08413766-4fa8-4eec-9392-ec30581fb48c", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T14:08:39", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: 88a48432-61c8-4444-bb6f-1d0174d4f177", -#> "fx": "DSI::datashield.login", -#> "session": "88a48432-61c8-4444-bb6f-1d0174d4f177", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T14:08:39", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "88a48432-61c8-4444-bb6f-1d0174d4f177", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T14:08:40", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "88a48432-61c8-4444-bb6f-1d0174d4f177", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T14:18:40", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: e45b3573-21cc-445d-8d22-b7fc289d279a", -#> "fx": "DSI::datashield.login", -#> "session": "e45b3573-21cc-445d-8d22-b7fc289d279a", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T14:18:40", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "e45b3573-21cc-445d-8d22-b7fc289d279a", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T14:18:41", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "e45b3573-21cc-445d-8d22-b7fc289d279a", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:04:13", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: 026b4b18-c40c-4f08-8a19-75716fee1c75", -#> "fx": "DSI::datashield.login", -#> "session": "026b4b18-c40c-4f08-8a19-75716fee1c75", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:04:13", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "026b4b18-c40c-4f08-8a19-75716fee1c75", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:04:14", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "026b4b18-c40c-4f08-8a19-75716fee1c75", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:13:01", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: a3a6f257-79ef-443c-a110-393b1ff196f0", -#> "fx": "DSI::datashield.login", -#> "session": "a3a6f257-79ef-443c-a110-393b1ff196f0", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:13:01", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "a3a6f257-79ef-443c-a110-393b1ff196f0", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:13:03", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "a3a6f257-79ef-443c-a110-393b1ff196f0", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:02", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "fx": "DSI::datashield.login", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:02", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:04", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:04", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "base::exists(\"dsROCrate_test\")", -#> "fx": "base::exists", -#> "symbol": "dsROCrate_test", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:04", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::classDS(\"dsROCrate_test\")", -#> "fx": "dsBase::classDS", -#> "symbol": "dsROCrate_test", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:04", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::isValidDS(dsROCrate_test)", -#> "fx": "dsBase::isValidDS", -#> "symbol": "dsROCrate_test", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:04", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::dimDS(\"dsROCrate_test\")", -#> "fx": "dsBase::dimDS", -#> "symbol": "dsROCrate_test", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:05", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::colnamesDS(\"dsROCrate_test\")", -#> "fx": "dsBase::colnamesDS", -#> "symbol": "dsROCrate_test", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> } -#> ] -#> ] -#> } -#> ] -#> } +# generate report +dsROCrate::report(proj_audit, title = "DataSHIELD Safe People - Audit Report") ``` -###### Markdown report +## Learn More -A markdown report can be created with an overview and details for an -RO-Crate, using the `dsROCrate::rocrate_report`: - -**Only generate .Rmd file** +For further details, see the following vignette: ``` r -safe_people_crate_v1_rmd <- tempfile(fileext = ".Rmd") # temporary file - -safe_people_crate_contents <- safe_people_crate_v1 |> - dsROCrate::rocrate_report(filepath = safe_people_crate_v1_rmd, render = FALSE) -#> 1 'Author' entity was found! -#> 3 'Dataset' entities were found! -#> Warning: No entities were found with @type = 'WriteAction'! -#> Warning: No entities were found with @type = 'ControlAction'! -#> 1 'Project' entity was found! -#> 14 'PropertyValue' OR 'SoftwareApplication' entities were found! -#> 2 'File' entities were found! - -# display Overview diagram -safe_people_crate_contents$overview_diagram +vignette("getting-started", package = "dsROCrate") ``` - - -``` r - -# display Overview data (Safe People, Safe Projects and Safe Data) -safe_people_crate_contents$overview_data |> - knitr::kable() -``` - -| Project | Data | Access Level | People | Function | Timestamp | -|:--------|:-------|:-------------|:-------|:-------------------|:--------------------| -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T08:56:26 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T08:56:27 | -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T09:00:37 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T09:00:38 | -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T09:20:48 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T09:20:49 | -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T14:08:39 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T14:08:40 | -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T14:18:40 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T14:18:41 | -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T15:04:13 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T15:04:14 | -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T15:13:01 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T15:13:03 | -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T15:24:02 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T15:24:04 | -| CNSIM | CNSIM1 | read | dsuser | base::exists | 2026-02-27T15:24:04 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::classDS | 2026-02-27T15:24:04 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::isValidDS | 2026-02-27T15:24:04 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::dimDS | 2026-02-27T15:24:04 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::colnamesDS | 2026-02-27T15:24:05 | -| CNSIM | CNSIM2 | read | dsuser | | | -| CNSIM | CNSIM3 | read | dsuser | | | - -**Render and display report (HTML)** - -``` r -safe_people_crate_v1 |> - dsROCrate::rocrate_report(filepath = safe_people_crate_v1_rmd, - title = "DataSHIELD Safe People - Audit Report", - render = TRUE, - overwrite = TRUE) -``` - -##### List all accessible projects & tables for an user - -``` r -safe_people_crate_v2 <- opalr::opal.login( - username = USERNAME, - password = USERPASS, - url = SERVER -) |> - dsROCrate::audit_safe_people(user = "dsuser") -#> opening file input connection. -#> Found 83 records... Imported 83 records. Simplifying... -#> closing file input connection. - -safe_people_crate_v2_rmd <- tempfile(fileext = ".Rmd") # temporary file - -safe_people_crate_contents_v2 <- safe_people_crate_v2 |> - dsROCrate::rocrate_report(filepath = safe_people_crate_v2_rmd, render = FALSE) -#> 1 'Author' entity was found! -#> 30 'Dataset' entities were found! -#> 11 'Project' entities were found! -#> 14 'PropertyValue' OR 'SoftwareApplication' entities were found! -#> 2 'File' entities were found! - -# display Overview diagram -safe_people_crate_contents_v2$overview_diagram -``` - - - -``` r - -# display Overview data (Safe People, Safe Projects and Safe Data) -safe_people_crate_contents_v2$overview_data |> - knitr::kable() -``` - -| Project | Data | Access Level | People | Function | Timestamp | -|:---|:---|:---|:---|:---|:---| -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T08:56:26 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T08:56:27 | -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T09:00:37 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T09:00:38 | -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T09:20:48 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T09:20:49 | -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T14:08:39 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T14:08:40 | -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T14:18:40 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T14:18:41 | -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T15:04:13 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T15:04:14 | -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T15:13:01 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T15:13:03 | -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T15:24:02 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T15:24:04 | -| CNSIM | CNSIM1 | read | dsuser | base::exists | 2026-02-27T15:24:04 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::classDS | 2026-02-27T15:24:04 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::isValidDS | 2026-02-27T15:24:04 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::dimDS | 2026-02-27T15:24:04 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::colnamesDS | 2026-02-27T15:24:05 | -| CNSIM | CNSIM2 | read | dsuser | | | -| CNSIM | CNSIM3 | read | dsuser | | | -| DASIM | DASIM1 | read | dsuser | | | -| DASIM | DASIM2 | read | dsuser | | | -| DASIM | DASIM3 | read | dsuser | | | -| DISCORDANT | DISCORDANT_STUDY1 | read | dsuser | | | -| DISCORDANT | DISCORDANT_STUDY2 | read | dsuser | | | -| DISCORDANT | DISCORDANT_STUDY3 | read | dsuser | | | -| GREENSPACE | Cohort1_exposome | read | dsuser | | | -| GREENSPACE | Cohort2_exposome | read | dsuser | | | -| GREENSPACE | Cohort3_exposome | read | dsuser | | | -| GWAS | ega_phenotypes | read | dsuser | | | -| GWAS | ega_phenotypes_1 | read | dsuser | | | -| GWAS | ega_phenotypes_2 | read | dsuser | | | -| GWAS | ega_phenotypes_3 | read | dsuser | | | -| MEDIATION | UPBdata1 | read | dsuser | | | -| MEDIATION | UPBdata2 | read | dsuser | | | -| MEDIATION | UPBdata3 | read | dsuser | | | -| SURVIVAL | EXPAND_WITH_MISSING1 | read | dsuser | | | -| SURVIVAL | EXPAND_WITH_MISSING2 | read | dsuser | | | -| SURVIVAL | EXPAND_WITH_MISSING3 | read | dsuser | | | -| TESTING | TESTING1 | read | dsuser | | | -| TESTING | TESTING2 | read | dsuser | | | -| TESTING | TESTING3 | read | dsuser | | | -| TITANIC_NEWCOMERS_WORKSHOP | titanic_server_1 | read | dsuser | | | -| TITANIC_NEWCOMERS_WORKSHOP | titanic_server_2 | read | dsuser | | | -| depression | growth_1 | read | dsuser | | | -| depression | growth_2 | read | dsuser | | | -| serverDataKey | myKey | read | dsuser | | | - -### 3.2. Audit Project - -##### List users and dataset/table level permissions within a project - -``` r -safe_project_crate_v1 <- opalr::opal.login( - username = USERNAME, - password = USERPASS, - url = SERVER -) |> - dsROCrate::audit_safe_project(project = "CNSIM") -#> opening file input connection. -#> Found 83 records... Imported 83 records. Simplifying... -#> closing file input connection. - -print(safe_project_crate_v1) -#> { -#> "@context": "https://w3id.org/ro/crate/1.2/context", -#> "@graph": [ -#> { -#> "@id": "ro-crate-metadata.json", -#> "@type": "CreativeWork", -#> "about": { -#> "@id": "./" -#> }, -#> "conformsTo": { -#> "@id": "https://w3id.org/ro/crate/1.2" -#> } -#> }, -#> { -#> "@id": "./", -#> "@type": "Dataset", -#> "name": "", -#> "description": "", -#> "datePublished": "2026-02-27", -#> "license": { -#> "@id": "http://spdx.org/licenses/CC-BY-4.0" -#> }, -#> "conformsTo": { -#> "@id": "https://w3id.org/5s-crate/0.4" -#> }, -#> "hasPart": [ -#> { -#> "@id": "20260227T152439-dslogs-dsuser.log" -#> }, -#> { -#> "@id": "20260227T152439-dslogs-dsuser_mappings.csv" -#> } -#> ] -#> }, -#> { -#> "@id": "https://w3id.org/5s-crate/0.4", -#> "@type": ["CreativeWork", "Profile"], -#> "name": "Five Safes RO-Crate profile" -#> }, -#> { -#> "@id": "#dataset:67adf2d8e106aca9b11de773758bd241", -#> "@type": "Dataset", -#> "name": "CNSIM1", -#> "dateCreated": "2026-02-27T06:29:50.731Z", -#> "dateModified": "2026-02-27T06:29:51.865Z", -#> "path": "/datasource/CNSIM/table/CNSIM1" -#> }, -#> { -#> "@id": "#dataset:ffb1b1adcafc024743be1b0c252787c9", -#> "@type": "Dataset", -#> "name": "CNSIM2", -#> "dateCreated": "2026-02-27T06:29:51.872Z", -#> "dateModified": "2026-02-27T06:29:53.057Z", -#> "path": "/datasource/CNSIM/table/CNSIM2" -#> }, -#> { -#> "@id": "#dataset:cc3061aef69ce457358815fb9d8c6492", -#> "@type": "Dataset", -#> "name": "CNSIM3", -#> "dateCreated": "2026-02-27T06:29:53.064Z", -#> "dateModified": "2026-02-27T06:29:54.200Z", -#> "path": "/datasource/CNSIM/table/CNSIM3" -#> }, -#> { -#> "@id": "#project:7ba189863f9f641196596cb28e04aa14", -#> "@type": "Project", -#> "name": "CNSIM", -#> "dateCreated": "2026-02-27T06:29:49.235Z", -#> "dateModified": "2026-02-27T06:29:54.200Z", -#> "hasPart": [ -#> { -#> "@id": "#dataset:67adf2d8e106aca9b11de773758bd241" -#> }, -#> { -#> "@id": "#dataset:ffb1b1adcafc024743be1b0c252787c9" -#> }, -#> { -#> "@id": "#dataset:cc3061aef69ce457358815fb9d8c6492" -#> } -#> ] -#> }, -#> { -#> "@id": "#person:a0af2a94926db1b49ad7a812eef509d2", -#> "@type": "Person", -#> "name": "dsuser" -#> }, -#> { -#> "@id": "#perm:9bf7f75b6c5b07d02830b95652cd39a0-dict-summary-read", -#> "@type": "ReadAction", -#> "agent": { -#> "@id": "#person:a0af2a94926db1b49ad7a812eef509d2" -#> }, -#> "object": { -#> "@id": "#dataset:67adf2d8e106aca9b11de773758bd241" -#> }, -#> "actionStatus": "PotentialActionStatus", -#> "description": "User may view table dictionary and summary statistics only; access to individual values is restricted." -#> }, -#> { -#> "@id": "#perm:363eb627d1e49c08933f2e26142e6d56-dict-summary-read", -#> "@type": "ReadAction", -#> "agent": { -#> "@id": "#person:a0af2a94926db1b49ad7a812eef509d2" -#> }, -#> "object": { -#> "@id": "#dataset:ffb1b1adcafc024743be1b0c252787c9" -#> }, -#> "actionStatus": "PotentialActionStatus", -#> "description": "User may view table dictionary and summary statistics only; access to individual values is restricted." -#> }, -#> { -#> "@id": "#perm:63b8097908f682bff1760e48d28c5855-dict-summary-read", -#> "@type": "ReadAction", -#> "agent": { -#> "@id": "#person:a0af2a94926db1b49ad7a812eef509d2" -#> }, -#> "object": { -#> "@id": "#dataset:cc3061aef69ce457358815fb9d8c6492" -#> }, -#> "actionStatus": "PotentialActionStatus", -#> "description": "User may view table dictionary and summary statistics only; access to individual values is restricted." -#> }, -#> { -#> "@id": "_:localid:datashield.privacyLevel:5", -#> "@type": "PropertyValue", -#> "name": "datashield.privacyLevel", -#> "value": "5" -#> }, -#> { -#> "@id": "_:localid:default.datashield.privacyControlLevel:banana", -#> "@type": "PropertyValue", -#> "name": "default.datashield.privacyControlLevel", -#> "value": "banana" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.glm:0.33", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.glm", -#> "value": "0.33" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.kNN:3", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.kNN", -#> "value": "3" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.levels.density:0.33", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.levels.density", -#> "value": "0.33" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.levels.max:40", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.levels.max", -#> "value": "40" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.noise:0.25", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.noise", -#> "value": "0.25" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.string:80", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.string", -#> "value": "80" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.stringShort:20", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.stringShort", -#> "value": "20" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.subset:3", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.subset", -#> "value": "3" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.tab:3", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.tab", -#> "value": "3" -#> }, -#> { -#> "@id": "_:localid:3aaeab3631b4c9bbe7f44d60805b8f9c", -#> "@type": "SoftwareApplication", -#> "name": "dsBase", -#> "version": "6.3.5", -#> "description": "Base 'DataSHIELD' functions for the server side. 'DataSHIELD' is a software package which allows\n you to do non-disclosive federated analysis on sensitive data. 'DataSHIELD' analytic functions have\n been designed to only share non disclosive summary statistics, with built in automated output\n checking based on statistical disclosure control. With data sites setting the threshold values for\n the automated output checks. For more details, see 'citation(\"dsBase\")'." -#> }, -#> { -#> "@id": "_:localid:e2f7c43973c40d7a6a6731da5a0aa564", -#> "@type": "SoftwareApplication", -#> "name": "dsTidyverse", -#> "version": "1.1.0", -#> "description": "Implementation of selected 'Tidyverse' functions within 'DataSHIELD', an open-source federated analysis solution in R. Currently, DataSHIELD contains very limited tools for data manipulation, so the aim of this package is to improve the researcher experience by implementing essential functions for data manipulation, including subsetting, filtering, grouping, and renaming variables. This is the serverside package which should be installed on the server holding the data, and is used in conjuncture with the clientside package 'dsTidyverseClient' which is installed in the local R environment of the analyst. For more information, see and ." -#> }, -#> { -#> "@id": "_:localid:411453e2513e6d909fe2cb8273b034dc", -#> "@type": "SoftwareApplication", -#> "name": "resourcer", -#> "version": "1.5.0", -#> "description": "A resource represents some data or a computation unit. It is \n described by a URL and credentials. This package proposes a Resource model\n with \"resolver\" and \"client\" classes to facilitate the access and the usage of the \n resources." -#> }, -#> { -#> "@id": "20260227T152439-dslogs-dsuser.log", -#> "@type": "File", -#> "dateModified": "2026-02-27 15:24:39", -#> "name": "20260227T152439-dslogs-dsuser.log", -#> "description": "This file contains the raw logs for the user: `dsuser` , between: ALL and ALL", -#> "encodingFormat": "text/plain", -#> "content": [ -#> ["[INFO][2026-02-27T08:56:24][OPEN] created a datashield session 622f2c74-9242-48e2-82d9-bc3250f8aa4b", "[INFO][2026-02-27T08:56:26][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T08:56:26][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T08:56:27][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T09:00:37][OPEN] created a datashield session 7925f5e8-f7b6-416e-b05b-5ac2c19f00c1", "[INFO][2026-02-27T09:00:37][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T09:00:38][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T09:00:38][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T09:20:47][OPEN] created a datashield session 08413766-4fa8-4eec-9392-ec30581fb48c", "[INFO][2026-02-27T09:20:48][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T09:20:48][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T09:20:49][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T14:08:39][OPEN] created a datashield session 88a48432-61c8-4444-bb6f-1d0174d4f177", "[INFO][2026-02-27T14:08:39][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T14:08:39][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T14:08:40][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T14:18:40][OPEN] created a datashield session e45b3573-21cc-445d-8d22-b7fc289d279a", "[INFO][2026-02-27T14:18:40][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T14:18:40][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T14:18:41][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:04:13][OPEN] created a datashield session 026b4b18-c40c-4f08-8a19-75716fee1c75", "[INFO][2026-02-27T15:04:13][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T15:04:14][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:04:14][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:13:01][OPEN] created a datashield session a3a6f257-79ef-443c-a110-393b1ff196f0", "[INFO][2026-02-27T15:13:01][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T15:13:02][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:13:03][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:24:02][OPEN] created a datashield session fc258313-6846-4a8e-93d9-4809cd794fbe", "[INFO][2026-02-27T15:24:02][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T15:24:03][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:24:04][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:24:04][PARSE] parsed 'base::exists(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][AGGREGATE] evaluated 'base::exists(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][PARSE] parsed 'dsBase::classDS(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][AGGREGATE] evaluated 'dsBase::classDS(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][PARSE] parsed 'dsBase::isValidDS(dsROCrate_test)'", "[INFO][2026-02-27T15:24:04][AGGREGATE] evaluated 'dsBase::isValidDS(dsROCrate_test)'", "[INFO][2026-02-27T15:24:04][PARSE] parsed 'dsBase::dimDS(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][AGGREGATE] evaluated 'dsBase::dimDS(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][PARSE] parsed 'dsBase::colnamesDS(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:05][AGGREGATE] evaluated 'dsBase::colnamesDS(\"dsROCrate_test\")'"] -#> ] -#> }, -#> { -#> "@id": "20260227T152439-dslogs-dsuser_mappings.csv", -#> "@type": "File", -#> "dateModified": "2026-02-27 15:24:39", -#> "name": "20260227T152439-dslogs-dsuser_mappings.csv", -#> "description": "This file contains mappings and evaluated functions", -#> "encodingFormat": "text/csv", -#> "content": [ -#> [ -#> { -#> "timestamp": "2026-02-27T08:56:24", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: 622f2c74-9242-48e2-82d9-bc3250f8aa4b", -#> "fx": "DSI::datashield.login", -#> "session": "622f2c74-9242-48e2-82d9-bc3250f8aa4b", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T08:56:26", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "622f2c74-9242-48e2-82d9-bc3250f8aa4b", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T08:56:27", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "622f2c74-9242-48e2-82d9-bc3250f8aa4b", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T09:00:37", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: 7925f5e8-f7b6-416e-b05b-5ac2c19f00c1", -#> "fx": "DSI::datashield.login", -#> "session": "7925f5e8-f7b6-416e-b05b-5ac2c19f00c1", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T09:00:37", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "7925f5e8-f7b6-416e-b05b-5ac2c19f00c1", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T09:00:38", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "7925f5e8-f7b6-416e-b05b-5ac2c19f00c1", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T09:20:47", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: 08413766-4fa8-4eec-9392-ec30581fb48c", -#> "fx": "DSI::datashield.login", -#> "session": "08413766-4fa8-4eec-9392-ec30581fb48c", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T09:20:48", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "08413766-4fa8-4eec-9392-ec30581fb48c", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T09:20:49", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "08413766-4fa8-4eec-9392-ec30581fb48c", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T14:08:39", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: 88a48432-61c8-4444-bb6f-1d0174d4f177", -#> "fx": "DSI::datashield.login", -#> "session": "88a48432-61c8-4444-bb6f-1d0174d4f177", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T14:08:39", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "88a48432-61c8-4444-bb6f-1d0174d4f177", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T14:08:40", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "88a48432-61c8-4444-bb6f-1d0174d4f177", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T14:18:40", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: e45b3573-21cc-445d-8d22-b7fc289d279a", -#> "fx": "DSI::datashield.login", -#> "session": "e45b3573-21cc-445d-8d22-b7fc289d279a", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T14:18:40", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "e45b3573-21cc-445d-8d22-b7fc289d279a", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T14:18:41", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "e45b3573-21cc-445d-8d22-b7fc289d279a", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:04:13", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: 026b4b18-c40c-4f08-8a19-75716fee1c75", -#> "fx": "DSI::datashield.login", -#> "session": "026b4b18-c40c-4f08-8a19-75716fee1c75", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:04:13", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "026b4b18-c40c-4f08-8a19-75716fee1c75", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:04:14", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "026b4b18-c40c-4f08-8a19-75716fee1c75", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:13:01", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: a3a6f257-79ef-443c-a110-393b1ff196f0", -#> "fx": "DSI::datashield.login", -#> "session": "a3a6f257-79ef-443c-a110-393b1ff196f0", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:13:01", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "a3a6f257-79ef-443c-a110-393b1ff196f0", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:13:03", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "a3a6f257-79ef-443c-a110-393b1ff196f0", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:02", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "fx": "DSI::datashield.login", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:02", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:04", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:04", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "base::exists(\"dsROCrate_test\")", -#> "fx": "base::exists", -#> "symbol": "dsROCrate_test", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:04", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::classDS(\"dsROCrate_test\")", -#> "fx": "dsBase::classDS", -#> "symbol": "dsROCrate_test", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:04", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::isValidDS(dsROCrate_test)", -#> "fx": "dsBase::isValidDS", -#> "symbol": "dsROCrate_test", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:04", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::dimDS(\"dsROCrate_test\")", -#> "fx": "dsBase::dimDS", -#> "symbol": "dsROCrate_test", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:05", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::colnamesDS(\"dsROCrate_test\")", -#> "fx": "dsBase::colnamesDS", -#> "symbol": "dsROCrate_test", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> } -#> ] -#> ] -#> } -#> ] -#> } -``` - -###### Markdown report - -A markdown report can be created with an overview and details for an -RO-Crate, using the `dsROCrate::rocrate_report`: - -**Only generate .Rmd file** - -``` r -safe_project_crate_v1_rmd <- tempfile(fileext = ".Rmd") # temporary file - -safe_project_crate_contents <- safe_project_crate_v1 |> - dsROCrate::rocrate_report(filepath = safe_project_crate_v1_rmd, render = FALSE) -#> 1 'Author' entity was found! -#> 3 'Dataset' entities were found! -#> Warning: No entities were found with @type = 'WriteAction'! -#> Warning: No entities were found with @type = 'ControlAction'! -#> 1 'Project' entity was found! -#> 14 'PropertyValue' OR 'SoftwareApplication' entities were found! -#> 2 'File' entities were found! - -# display Overview diagram -safe_project_crate_contents$overview_diagram -``` - - - -``` r - -# display Overview data (Safe People, Safe Projects and Safe Data) -safe_project_crate_contents$overview_data |> - knitr::kable() -``` - -| Project | Data | Access Level | People | Function | Timestamp | -|:--------|:-------|:-------------|:-------|:-------------------|:--------------------| -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T08:56:26 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T08:56:27 | -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T09:00:37 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T09:00:38 | -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T09:20:48 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T09:20:49 | -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T14:08:39 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T14:08:40 | -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T14:18:40 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T14:18:41 | -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T15:04:13 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T15:04:14 | -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T15:13:01 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T15:13:03 | -| CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T15:24:02 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T15:24:04 | -| CNSIM | CNSIM1 | read | dsuser | base::exists | 2026-02-27T15:24:04 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::classDS | 2026-02-27T15:24:04 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::isValidDS | 2026-02-27T15:24:04 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::dimDS | 2026-02-27T15:24:04 | -| CNSIM | CNSIM1 | read | dsuser | dsBase::colnamesDS | 2026-02-27T15:24:05 | -| CNSIM | CNSIM2 | read | dsuser | | | -| CNSIM | CNSIM3 | read | dsuser | | | - -**Render and display report (HTML)** - -``` r -safe_project_crate_v1 |> - dsROCrate::rocrate_report(filepath = safe_project_crate_v1_rmd, - title = "DataSHIELD Safe Project - Audit Report", - render = TRUE, - overwrite = TRUE) -``` - -
- -### 3.3. Audit Study - -##### List users and dataset/table level permissions within a study (i.e., multiple servers) - -``` r -study_crate_v1 <- - list( - "opal_test" = opalr::opal.login( - username = USERNAME, - password = USERPASS, - url = "https://opal-test.obiba.org" - ), - "opal_demo" = opalr::opal.login( - username = USERNAME, - password = USERPASS, - url = "https://opal-demo.obiba.org" - ) - ) |> - dsROCrate::audit_study(project = "CNSIM") - -print(study_crate_v1) -#> $opal_test -#> { -#> "@context": "https://w3id.org/ro/crate/1.2/context", -#> "@graph": [ -#> { -#> "@id": "ro-crate-metadata.json", -#> "@type": "CreativeWork", -#> "about": { -#> "@id": "./" -#> }, -#> "conformsTo": { -#> "@id": "https://w3id.org/ro/crate/1.2" -#> } -#> }, -#> { -#> "@id": "./", -#> "@type": "Dataset", -#> "name": "", -#> "description": "", -#> "datePublished": "2026-02-27", -#> "license": { -#> "@id": "http://spdx.org/licenses/CC-BY-4.0" -#> }, -#> "conformsTo": { -#> "@id": "https://w3id.org/5s-crate/0.4" -#> } -#> }, -#> { -#> "@id": "https://w3id.org/5s-crate/0.4", -#> "@type": ["CreativeWork", "Profile"], -#> "name": "Five Safes RO-Crate profile" -#> }, -#> { -#> "@id": "#dataset:67adf2d8e106aca9b11de773758bd241", -#> "@type": "Dataset", -#> "name": "CNSIM1", -#> "dateCreated": "2026-01-17T10:53:48.925Z", -#> "dateModified": "2026-01-17T10:53:50.189Z", -#> "path": "/datasource/CNSIM/table/CNSIM1" -#> }, -#> { -#> "@id": "#dataset:ffb1b1adcafc024743be1b0c252787c9", -#> "@type": "Dataset", -#> "name": "CNSIM2", -#> "dateCreated": "2026-01-17T10:53:50.194Z", -#> "dateModified": "2026-01-17T10:53:51.417Z", -#> "path": "/datasource/CNSIM/table/CNSIM2" -#> }, -#> { -#> "@id": "#dataset:cc3061aef69ce457358815fb9d8c6492", -#> "@type": "Dataset", -#> "name": "CNSIM3", -#> "dateCreated": "2026-01-17T10:53:51.422Z", -#> "dateModified": "2026-01-17T10:53:52.663Z", -#> "path": "/datasource/CNSIM/table/CNSIM3" -#> }, -#> { -#> "@id": "#project:7ba189863f9f641196596cb28e04aa14", -#> "@type": "Project", -#> "name": "CNSIM", -#> "dateCreated": "2026-01-08T17:40:46.773Z", -#> "dateModified": "2026-01-17T10:53:52.663Z", -#> "hasPart": [ -#> { -#> "@id": "#dataset:67adf2d8e106aca9b11de773758bd241" -#> }, -#> { -#> "@id": "#dataset:ffb1b1adcafc024743be1b0c252787c9" -#> }, -#> { -#> "@id": "#dataset:cc3061aef69ce457358815fb9d8c6492" -#> } -#> ] -#> }, -#> { -#> "@id": "#person:a0af2a94926db1b49ad7a812eef509d2", -#> "@type": "Person", -#> "name": "dsuser" -#> }, -#> { -#> "@id": "#perm:9bf7f75b6c5b07d02830b95652cd39a0-dict-summary-read", -#> "@type": "ReadAction", -#> "agent": { -#> "@id": "#person:a0af2a94926db1b49ad7a812eef509d2" -#> }, -#> "object": { -#> "@id": "#dataset:67adf2d8e106aca9b11de773758bd241" -#> }, -#> "actionStatus": "PotentialActionStatus", -#> "description": "User may view table dictionary and summary statistics only; access to individual values is restricted." -#> }, -#> { -#> "@id": "_:localid:datashield.privacyLevel:5", -#> "@type": "PropertyValue", -#> "name": "datashield.privacyLevel", -#> "value": "5" -#> }, -#> { -#> "@id": "_:localid:default.datashield.privacyControlLevel:banana", -#> "@type": "PropertyValue", -#> "name": "default.datashield.privacyControlLevel", -#> "value": "banana" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.glm:0.33", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.glm", -#> "value": "0.33" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.kNN:3", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.kNN", -#> "value": "3" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.levels.density:0.33", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.levels.density", -#> "value": "0.33" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.levels.max:40", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.levels.max", -#> "value": "40" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.noise:0.25", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.noise", -#> "value": "0.25" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.string:80", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.string", -#> "value": "80" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.stringShort:20", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.stringShort", -#> "value": "20" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.subset:3", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.subset", -#> "value": "3" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.tab:3", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.tab", -#> "value": "3" -#> }, -#> { -#> "@id": "_:localid:3aaeab3631b4c9bbe7f44d60805b8f9c", -#> "@type": "SoftwareApplication", -#> "name": "dsBase", -#> "version": "6.3.5", -#> "description": "Base 'DataSHIELD' functions for the server side. 'DataSHIELD' is a software package which allows\n you to do non-disclosive federated analysis on sensitive data. 'DataSHIELD' analytic functions have\n been designed to only share non disclosive summary statistics, with built in automated output\n checking based on statistical disclosure control. With data sites setting the threshold values for\n the automated output checks. For more details, see 'citation(\"dsBase\")'." -#> }, -#> { -#> "@id": "_:localid:e2f7c43973c40d7a6a6731da5a0aa564", -#> "@type": "SoftwareApplication", -#> "name": "dsTidyverse", -#> "version": "1.1.0", -#> "description": "Implementation of selected 'Tidyverse' functions within 'DataSHIELD', an open-source federated analysis solution in R. Currently, DataSHIELD contains very limited tools for data manipulation, so the aim of this package is to improve the researcher experience by implementing essential functions for data manipulation, including subsetting, filtering, grouping, and renaming variables. This is the serverside package which should be installed on the server holding the data, and is used in conjuncture with the clientside package 'dsTidyverseClient' which is installed in the local R environment of the analyst. For more information, see and ." -#> }, -#> { -#> "@id": "_:localid:411453e2513e6d909fe2cb8273b034dc", -#> "@type": "SoftwareApplication", -#> "name": "resourcer", -#> "version": "1.5.0", -#> "description": "A resource represents some data or a computation unit. It is \n described by a URL and credentials. This package proposes a Resource model\n with \"resolver\" and \"client\" classes to facilitate the access and the usage of the \n resources." -#> } -#> ] -#> } -#> -#> $opal_demo -#> { -#> "@context": "https://w3id.org/ro/crate/1.2/context", -#> "@graph": [ -#> { -#> "@id": "ro-crate-metadata.json", -#> "@type": "CreativeWork", -#> "about": { -#> "@id": "./" -#> }, -#> "conformsTo": { -#> "@id": "https://w3id.org/ro/crate/1.2" -#> } -#> }, -#> { -#> "@id": "./", -#> "@type": "Dataset", -#> "name": "", -#> "description": "", -#> "datePublished": "2026-02-27", -#> "license": { -#> "@id": "http://spdx.org/licenses/CC-BY-4.0" -#> }, -#> "conformsTo": { -#> "@id": "https://w3id.org/5s-crate/0.4" -#> }, -#> "hasPart": [ -#> { -#> "@id": "20260227T152444-dslogs-dsuser.log" -#> }, -#> { -#> "@id": "20260227T152444-dslogs-dsuser_mappings.csv" -#> } -#> ] -#> }, -#> { -#> "@id": "https://w3id.org/5s-crate/0.4", -#> "@type": ["CreativeWork", "Profile"], -#> "name": "Five Safes RO-Crate profile" -#> }, -#> { -#> "@id": "#dataset:67adf2d8e106aca9b11de773758bd241", -#> "@type": "Dataset", -#> "name": "CNSIM1", -#> "dateCreated": "2026-02-27T06:29:50.731Z", -#> "dateModified": "2026-02-27T06:29:51.865Z", -#> "path": "/datasource/CNSIM/table/CNSIM1" -#> }, -#> { -#> "@id": "#dataset:ffb1b1adcafc024743be1b0c252787c9", -#> "@type": "Dataset", -#> "name": "CNSIM2", -#> "dateCreated": "2026-02-27T06:29:51.872Z", -#> "dateModified": "2026-02-27T06:29:53.057Z", -#> "path": "/datasource/CNSIM/table/CNSIM2" -#> }, -#> { -#> "@id": "#dataset:cc3061aef69ce457358815fb9d8c6492", -#> "@type": "Dataset", -#> "name": "CNSIM3", -#> "dateCreated": "2026-02-27T06:29:53.064Z", -#> "dateModified": "2026-02-27T06:29:54.200Z", -#> "path": "/datasource/CNSIM/table/CNSIM3" -#> }, -#> { -#> "@id": "#project:7ba189863f9f641196596cb28e04aa14", -#> "@type": "Project", -#> "name": "CNSIM", -#> "dateCreated": "2026-02-27T06:29:49.235Z", -#> "dateModified": "2026-02-27T06:29:54.200Z", -#> "hasPart": [ -#> { -#> "@id": "#dataset:67adf2d8e106aca9b11de773758bd241" -#> }, -#> { -#> "@id": "#dataset:ffb1b1adcafc024743be1b0c252787c9" -#> }, -#> { -#> "@id": "#dataset:cc3061aef69ce457358815fb9d8c6492" -#> } -#> ] -#> }, -#> { -#> "@id": "#person:a0af2a94926db1b49ad7a812eef509d2", -#> "@type": "Person", -#> "name": "dsuser" -#> }, -#> { -#> "@id": "#perm:9bf7f75b6c5b07d02830b95652cd39a0-dict-summary-read", -#> "@type": "ReadAction", -#> "agent": { -#> "@id": "#person:a0af2a94926db1b49ad7a812eef509d2" -#> }, -#> "object": { -#> "@id": "#dataset:67adf2d8e106aca9b11de773758bd241" -#> }, -#> "actionStatus": "PotentialActionStatus", -#> "description": "User may view table dictionary and summary statistics only; access to individual values is restricted." -#> }, -#> { -#> "@id": "#perm:363eb627d1e49c08933f2e26142e6d56-dict-summary-read", -#> "@type": "ReadAction", -#> "agent": { -#> "@id": "#person:a0af2a94926db1b49ad7a812eef509d2" -#> }, -#> "object": { -#> "@id": "#dataset:ffb1b1adcafc024743be1b0c252787c9" -#> }, -#> "actionStatus": "PotentialActionStatus", -#> "description": "User may view table dictionary and summary statistics only; access to individual values is restricted." -#> }, -#> { -#> "@id": "#perm:63b8097908f682bff1760e48d28c5855-dict-summary-read", -#> "@type": "ReadAction", -#> "agent": { -#> "@id": "#person:a0af2a94926db1b49ad7a812eef509d2" -#> }, -#> "object": { -#> "@id": "#dataset:cc3061aef69ce457358815fb9d8c6492" -#> }, -#> "actionStatus": "PotentialActionStatus", -#> "description": "User may view table dictionary and summary statistics only; access to individual values is restricted." -#> }, -#> { -#> "@id": "_:localid:datashield.privacyLevel:5", -#> "@type": "PropertyValue", -#> "name": "datashield.privacyLevel", -#> "value": "5" -#> }, -#> { -#> "@id": "_:localid:default.datashield.privacyControlLevel:banana", -#> "@type": "PropertyValue", -#> "name": "default.datashield.privacyControlLevel", -#> "value": "banana" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.glm:0.33", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.glm", -#> "value": "0.33" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.kNN:3", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.kNN", -#> "value": "3" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.levels.density:0.33", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.levels.density", -#> "value": "0.33" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.levels.max:40", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.levels.max", -#> "value": "40" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.noise:0.25", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.noise", -#> "value": "0.25" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.string:80", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.string", -#> "value": "80" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.stringShort:20", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.stringShort", -#> "value": "20" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.subset:3", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.subset", -#> "value": "3" -#> }, -#> { -#> "@id": "_:localid:default.nfilter.tab:3", -#> "@type": "PropertyValue", -#> "name": "default.nfilter.tab", -#> "value": "3" -#> }, -#> { -#> "@id": "_:localid:3aaeab3631b4c9bbe7f44d60805b8f9c", -#> "@type": "SoftwareApplication", -#> "name": "dsBase", -#> "version": "6.3.5", -#> "description": "Base 'DataSHIELD' functions for the server side. 'DataSHIELD' is a software package which allows\n you to do non-disclosive federated analysis on sensitive data. 'DataSHIELD' analytic functions have\n been designed to only share non disclosive summary statistics, with built in automated output\n checking based on statistical disclosure control. With data sites setting the threshold values for\n the automated output checks. For more details, see 'citation(\"dsBase\")'." -#> }, -#> { -#> "@id": "_:localid:e2f7c43973c40d7a6a6731da5a0aa564", -#> "@type": "SoftwareApplication", -#> "name": "dsTidyverse", -#> "version": "1.1.0", -#> "description": "Implementation of selected 'Tidyverse' functions within 'DataSHIELD', an open-source federated analysis solution in R. Currently, DataSHIELD contains very limited tools for data manipulation, so the aim of this package is to improve the researcher experience by implementing essential functions for data manipulation, including subsetting, filtering, grouping, and renaming variables. This is the serverside package which should be installed on the server holding the data, and is used in conjuncture with the clientside package 'dsTidyverseClient' which is installed in the local R environment of the analyst. For more information, see and ." -#> }, -#> { -#> "@id": "_:localid:411453e2513e6d909fe2cb8273b034dc", -#> "@type": "SoftwareApplication", -#> "name": "resourcer", -#> "version": "1.5.0", -#> "description": "A resource represents some data or a computation unit. It is \n described by a URL and credentials. This package proposes a Resource model\n with \"resolver\" and \"client\" classes to facilitate the access and the usage of the \n resources." -#> }, -#> { -#> "@id": "20260227T152444-dslogs-dsuser.log", -#> "@type": "File", -#> "dateModified": "2026-02-27 15:24:44", -#> "name": "20260227T152444-dslogs-dsuser.log", -#> "description": "This file contains the raw logs for the user: `dsuser` , between: ALL and ALL", -#> "encodingFormat": "text/plain", -#> "content": [ -#> ["[INFO][2026-02-27T08:56:24][OPEN] created a datashield session 622f2c74-9242-48e2-82d9-bc3250f8aa4b", "[INFO][2026-02-27T08:56:26][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T08:56:26][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T08:56:27][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T09:00:37][OPEN] created a datashield session 7925f5e8-f7b6-416e-b05b-5ac2c19f00c1", "[INFO][2026-02-27T09:00:37][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T09:00:38][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T09:00:38][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T09:20:47][OPEN] created a datashield session 08413766-4fa8-4eec-9392-ec30581fb48c", "[INFO][2026-02-27T09:20:48][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T09:20:48][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T09:20:49][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T14:08:39][OPEN] created a datashield session 88a48432-61c8-4444-bb6f-1d0174d4f177", "[INFO][2026-02-27T14:08:39][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T14:08:39][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T14:08:40][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T14:18:40][OPEN] created a datashield session e45b3573-21cc-445d-8d22-b7fc289d279a", "[INFO][2026-02-27T14:18:40][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T14:18:40][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T14:18:41][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:04:13][OPEN] created a datashield session 026b4b18-c40c-4f08-8a19-75716fee1c75", "[INFO][2026-02-27T15:04:13][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T15:04:14][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:04:14][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:13:01][OPEN] created a datashield session a3a6f257-79ef-443c-a110-393b1ff196f0", "[INFO][2026-02-27T15:13:01][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T15:13:02][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:13:03][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:24:02][OPEN] created a datashield session fc258313-6846-4a8e-93d9-4809cd794fbe", "[INFO][2026-02-27T15:24:02][ASSIGN] created symbol 'dsROCrate_test' from: 'dsROCrate_test <- opal[CNSIM.CNSIM1]'", "[INFO][2026-02-27T15:24:03][PARSE] parsed 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:24:04][AGGREGATE] evaluated 'dsBase::lsDS(search.filter = NULL, 1L)'", "[INFO][2026-02-27T15:24:04][PARSE] parsed 'base::exists(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][AGGREGATE] evaluated 'base::exists(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][PARSE] parsed 'dsBase::classDS(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][AGGREGATE] evaluated 'dsBase::classDS(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][PARSE] parsed 'dsBase::isValidDS(dsROCrate_test)'", "[INFO][2026-02-27T15:24:04][AGGREGATE] evaluated 'dsBase::isValidDS(dsROCrate_test)'", "[INFO][2026-02-27T15:24:04][PARSE] parsed 'dsBase::dimDS(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][AGGREGATE] evaluated 'dsBase::dimDS(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:04][PARSE] parsed 'dsBase::colnamesDS(\"dsROCrate_test\")'", "[INFO][2026-02-27T15:24:05][AGGREGATE] evaluated 'dsBase::colnamesDS(\"dsROCrate_test\")'"] -#> ] -#> }, -#> { -#> "@id": "20260227T152444-dslogs-dsuser_mappings.csv", -#> "@type": "File", -#> "dateModified": "2026-02-27 15:24:44", -#> "name": "20260227T152444-dslogs-dsuser_mappings.csv", -#> "description": "This file contains mappings and evaluated functions", -#> "encodingFormat": "text/csv", -#> "content": [ -#> [ -#> { -#> "timestamp": "2026-02-27T08:56:24", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: 622f2c74-9242-48e2-82d9-bc3250f8aa4b", -#> "fx": "DSI::datashield.login", -#> "session": "622f2c74-9242-48e2-82d9-bc3250f8aa4b", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T08:56:26", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "622f2c74-9242-48e2-82d9-bc3250f8aa4b", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T08:56:27", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "622f2c74-9242-48e2-82d9-bc3250f8aa4b", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T09:00:37", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: 7925f5e8-f7b6-416e-b05b-5ac2c19f00c1", -#> "fx": "DSI::datashield.login", -#> "session": "7925f5e8-f7b6-416e-b05b-5ac2c19f00c1", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T09:00:37", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "7925f5e8-f7b6-416e-b05b-5ac2c19f00c1", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T09:00:38", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "7925f5e8-f7b6-416e-b05b-5ac2c19f00c1", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T09:20:47", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: 08413766-4fa8-4eec-9392-ec30581fb48c", -#> "fx": "DSI::datashield.login", -#> "session": "08413766-4fa8-4eec-9392-ec30581fb48c", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T09:20:48", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "08413766-4fa8-4eec-9392-ec30581fb48c", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T09:20:49", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "08413766-4fa8-4eec-9392-ec30581fb48c", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T14:08:39", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: 88a48432-61c8-4444-bb6f-1d0174d4f177", -#> "fx": "DSI::datashield.login", -#> "session": "88a48432-61c8-4444-bb6f-1d0174d4f177", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T14:08:39", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "88a48432-61c8-4444-bb6f-1d0174d4f177", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T14:08:40", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "88a48432-61c8-4444-bb6f-1d0174d4f177", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T14:18:40", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: e45b3573-21cc-445d-8d22-b7fc289d279a", -#> "fx": "DSI::datashield.login", -#> "session": "e45b3573-21cc-445d-8d22-b7fc289d279a", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T14:18:40", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "e45b3573-21cc-445d-8d22-b7fc289d279a", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T14:18:41", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "e45b3573-21cc-445d-8d22-b7fc289d279a", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:04:13", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: 026b4b18-c40c-4f08-8a19-75716fee1c75", -#> "fx": "DSI::datashield.login", -#> "session": "026b4b18-c40c-4f08-8a19-75716fee1c75", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:04:13", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "026b4b18-c40c-4f08-8a19-75716fee1c75", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:04:14", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "026b4b18-c40c-4f08-8a19-75716fee1c75", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:13:01", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: a3a6f257-79ef-443c-a110-393b1ff196f0", -#> "fx": "DSI::datashield.login", -#> "session": "a3a6f257-79ef-443c-a110-393b1ff196f0", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:13:01", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "a3a6f257-79ef-443c-a110-393b1ff196f0", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:13:03", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "a3a6f257-79ef-443c-a110-393b1ff196f0", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:02", -#> "action": "OPEN", -#> "user": "dsuser", -#> "r_cmd": "Open session: fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "fx": "DSI::datashield.login", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:02", -#> "action": "ASSIGN", -#> "user": "dsuser", -#> "r_cmd": "dsROCrate_test <- opal[CNSIM.CNSIM1]", -#> "fx": "base::assign", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:04", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::lsDS(search.filter = NULL, 1L)", -#> "fx": "dsBase::lsDS", -#> "symbol": "search.filter = NULL, 1L", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:04", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "base::exists(\"dsROCrate_test\")", -#> "fx": "base::exists", -#> "symbol": "dsROCrate_test", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:04", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::classDS(\"dsROCrate_test\")", -#> "fx": "dsBase::classDS", -#> "symbol": "dsROCrate_test", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:04", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::isValidDS(dsROCrate_test)", -#> "fx": "dsBase::isValidDS", -#> "symbol": "dsROCrate_test", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:04", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::dimDS(\"dsROCrate_test\")", -#> "fx": "dsBase::dimDS", -#> "symbol": "dsROCrate_test", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> }, -#> { -#> "timestamp": "2026-02-27T15:24:05", -#> "action": "AGGREGATE", -#> "user": "dsuser", -#> "r_cmd": "dsBase::colnamesDS(\"dsROCrate_test\")", -#> "fx": "dsBase::colnamesDS", -#> "symbol": "dsROCrate_test", -#> "table": "CNSIM.CNSIM1", -#> "session": "fc258313-6846-4a8e-93d9-4809cd794fbe", -#> "backend": "OBiBa's Opal" -#> } -#> ] -#> ] -#> } -#> ] -#> } -#> -#> attr(,"audit_type") -#> [1] "Study" -#> attr(,"project") -#> [1] "CNSIM" -``` - -###### Markdown report - -A markdown report can be created with an overview and details for an -RO-Crate, using the `dsROCrate::rocrate_report`: - -**Only generate .Rmd file** - -``` r -study_crate_v1_rmd <- tempfile(fileext = ".Rmd") # temporary file - -safe_project_crate_contents <- study_crate_v1 |> - dsROCrate::rocrate_report(filepath = study_crate_v1_rmd, render = FALSE) -#> 1 'Author' entity was found! -#> 3 'Dataset' entities were found! -#> Warning: No entities were found with @type = 'WriteAction'! -#> Warning: No entities were found with @type = 'ControlAction'! -#> 1 'Project' entity was found! -#> 14 'PropertyValue' OR 'SoftwareApplication' entities were found! -#> Warning: No entities were found with @type = 'File'! -#> 1 'Author' entity was found! -#> 3 'Dataset' entities were found! -#> Warning: No entities were found with @type = 'WriteAction'! -#> Warning: No entities were found with @type = 'ControlAction'! -#> 1 'Project' entity was found! -#> 14 'PropertyValue' OR 'SoftwareApplication' entities were found! -#> 2 'File' entities were found! - -# display Overview diagram -safe_project_crate_contents$overview_diagram -``` - - - -``` r - -# display Overview data (Safe People, Safe Projects and Safe Data) -safe_project_crate_contents$overview_data |> - knitr::kable() -``` - -| Server | Project | Data | Access Level | People | Function | Timestamp | -|:---|:---|:---|:---|:---|:---|:---| -| opal_demo | CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T08:56:26 | -| opal_demo | CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T08:56:27 | -| opal_demo | CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T09:00:37 | -| opal_demo | CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T09:00:38 | -| opal_demo | CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T09:20:48 | -| opal_demo | CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T09:20:49 | -| opal_demo | CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T14:08:39 | -| opal_demo | CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T14:08:40 | -| opal_demo | CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T14:18:40 | -| opal_demo | CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T14:18:41 | -| opal_demo | CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T15:04:13 | -| opal_demo | CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T15:04:14 | -| opal_demo | CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T15:13:01 | -| opal_demo | CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T15:13:03 | -| opal_demo | CNSIM | CNSIM1 | read | dsuser | base::assign | 2026-02-27T15:24:02 | -| opal_demo | CNSIM | CNSIM1 | read | dsuser | dsBase::lsDS | 2026-02-27T15:24:04 | -| opal_demo | CNSIM | CNSIM1 | read | dsuser | base::exists | 2026-02-27T15:24:04 | -| opal_demo | CNSIM | CNSIM1 | read | dsuser | dsBase::classDS | 2026-02-27T15:24:04 | -| opal_demo | CNSIM | CNSIM1 | read | dsuser | dsBase::isValidDS | 2026-02-27T15:24:04 | -| opal_demo | CNSIM | CNSIM1 | read | dsuser | dsBase::dimDS | 2026-02-27T15:24:04 | -| opal_demo | CNSIM | CNSIM1 | read | dsuser | dsBase::colnamesDS | 2026-02-27T15:24:05 | -| opal_demo | CNSIM | CNSIM2 | read | dsuser | | | -| opal_demo | CNSIM | CNSIM3 | read | dsuser | | | -| opal_test | CNSIM | CNSIM1 | read | dsuser | | | - -**Render and display report (HTML)** - -``` r -study_crate_v1 |> - dsROCrate::rocrate_report(filepath = study_crate_v1_rmd, - title = "DataSHIELD Study audit", - render = TRUE, - overwrite = TRUE) -``` - -
- -## 4. Identity +## Identity You are welcome to use any of the following hex codes when referencing `{dsROCrate}`: diff --git a/cran-comments.md b/cran-comments.md deleted file mode 100644 index 858617d..0000000 --- a/cran-comments.md +++ /dev/null @@ -1,5 +0,0 @@ -## R CMD check results - -0 errors | 0 warnings | 1 note - -* This is a new release. diff --git a/inst/CITATION b/inst/CITATION new file mode 100644 index 0000000..8a0c5fc --- /dev/null +++ b/inst/CITATION @@ -0,0 +1,29 @@ +citHeader("To cite {dsROCrate} in publications use:") + +citation(auto = meta) + +bibentry( + bibtype = "Misc", + key = "focus5_2026", + title = "FOCUS-5: The feasibility of RO-Crates as a standards-based framework for auditing federated analyses and projects", + author = c( + person("Becca", "Wilson"), + person("Olly", "Butters"), + person("Vishnu Vardhan", "Chandrabalan"), + person("Dale", "Kirkwood"), + person("Matt", "Stammers"), + person("Mike", "Harding"), + person("Jake", "Harrington"), + person("Paula", "Irving"), + person("Yannick", "Marcon"), + person("Jamie", "O'Brien"), + person("Pawel", "Szmajda"), + person("Alwin K", "Thomas"), + person("Roberto", "Villegas-Diaz"), + person("Stuart", "Wheater") + ), + year = "2026", + note = "Zenodo report associated with the dsROCrate R package", + doi = "10.5281/zenodo.19366020", + url = "https://zenodo.org/doi/10.5281/zenodo.19366020" +) diff --git a/inst/WORDLIST b/inst/WORDLIST index b15c4c3..8540a5d 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -1,43 +1,38 @@ -CNSIM -Codecov -DASIM +Aalbersberg +CMD DataSHIELD DataSHIELD’ -GREENSPACE +Dumontier Lifecycle MOLGENIS -MOLGENIS’ OBiBa OBiBa's -OBiBa’s ORCID OllyButters +POSIXct RO Rmd SafePods +Sci TRE TREs Tibble -UPBdata Unfill -classDS -colnamesDS +bagit com +cr datashield -dimDS -dsBase -dsuser +doi etc -exposome github ies -isValidDS issuecomment -lsDS md +org pcr -tibble +sdata tidyr tidyverse +toml ’s ️NOTE diff --git a/man/append_entity_ref.Rd b/man/append_entity_ref.Rd new file mode 100644 index 0000000..79db726 --- /dev/null +++ b/man/append_entity_ref.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-rocrateR.R +\name{append_entity_ref} +\alias{append_entity_ref} +\title{Append entity reference} +\usage{ +append_entity_ref(rocrate, id, key, ref_id) +} +\arguments{ +\item{rocrate}{RO-Crate object.} + +\item{id}{String with \verb{@id} of the entity.} + +\item{key}{String with property/key of the entity.} + +\item{ref_id}{String with reference \verb{@id}.} +} +\value{ +Updated RO-Crate object. +} +\description{ +Adds a reference to an existing property while +preserving existing values and avoiding duplicates. +} +\keyword{internal} diff --git a/man/audit.Rd b/man/audit.Rd new file mode 100644 index 0000000..6d1e009 --- /dev/null +++ b/man/audit.Rd @@ -0,0 +1,90 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/audit.R +\name{audit} +\alias{audit} +\alias{audit.ArmadilloCredentials} +\alias{audit.character} +\alias{audit.cr8tor} +\alias{audit.list} +\alias{audit.opal} +\alias{audit.rocrate} +\title{Create an audit RO-Crate} +\usage{ +audit(x, ...) + +\method{audit}{ArmadilloCredentials}(x, ..., intent = NULL) + +\method{audit}{character}(x, ..., intent = NULL) + +\method{audit}{cr8tor}(x, ..., intent = NULL) + +\method{audit}{list}(x, ..., intent = NULL) + +\method{audit}{opal}( + x, + ..., + intent = NULL, + project = NULL, + user = NULL, + logs_from = -Inf, + logs_to = Inf, + path = NULL +) + +\method{audit}{rocrate}(x, ..., intent = NULL) +} +\arguments{ +\item{x}{Object to be audited. This can be +\itemize{ +\item a \emph{connection} to a DataSHIELD server (e.g., OBiBa's Opal). +\item a \emph{path} pointing to an RO-Crate OR a \code{cr8tor} archive / +. governance bundle. +\item an \emph{RO-Crate} object. +} +Alternatively, a list of any of the above.} + +\item{...}{Additional arguments.} + +\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 +details.} + +\item{user}{String with the user name for which to extract Safe People +details.} + +\item{logs_from}{Lower limit timestamp to filter out the outputs generated +(default: \code{-Inf}, everything up to \code{logs_to})} + +\item{logs_to}{Upper limit timestamp to filter out the outputs generated +(default: \code{Inf}, everything from \code{logs_from} onwards).} + +\item{path}{String with path pointing to the root of the RO-Crate. This will +be used to store log files. If not provided, logs will be stored within +the RO-Crate returned by this function.} +} +\value{ +RO-Crate with audit details. +} +\description{ +Create an audit RO-Crate following the 5 Safes Principles. +} +\details{ +This function handles various audit types, which will be dispatched based on +the input object. If the input object is + +\itemize{ +\item a \emph{connection} to a DataSHIELD server (e.g., OBiBa's Opal): +. generates an RO-Crate object with deployment details, including outputs. +\item a \emph{path} pointing to +\itemize{ +\item \strong{a \code{cr8tor} archive / governance bundle}: generates an +RO-Crate object with pre-deployment governance details. +\item \strong{an RO-Crate object}: generates an RO-Crate object with +clearly defined 5 Safes elements. +} +\item an \emph{RO-Crate} object: generates an RO-Crate object with clearly +defined 5 Safes elements. +} +} diff --git a/man/audit_cr8tor.Rd b/man/audit_cr8tor.Rd deleted file mode 100644 index 911dd71..0000000 --- a/man/audit_cr8tor.Rd +++ /dev/null @@ -1,24 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/audit_cr8tor.R -\name{audit_cr8tor} -\alias{audit_cr8tor} -\title{Audit cr8tor project archive} -\usage{ -audit_cr8tor(path, ...) -} -\arguments{ -\item{path}{Path to cr8tor archive.} - -\item{...}{Additional arguments for \link[rocrateR:load_rocrate]{rocrateR::load_rocrate}.} -} -\value{ -RO-Crate audit object -} -\description{ -This audit loads a cr8tor project archive and generates an RO-Crate object -with pre-deployment governance details. This then can be rendered with -\code{\link[=rocrate_report]{rocrate_report()}}. -} -\references{ -https://karectl-crates.github.io/cr8tor-metamodel/ -} diff --git a/man/audit_safe_people.Rd b/man/audit_engine.Rd similarity index 65% rename from man/audit_safe_people.Rd rename to man/audit_engine.Rd index 2ec7bda..f566f63 100644 --- a/man/audit_safe_people.Rd +++ b/man/audit_engine.Rd @@ -1,20 +1,20 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/audit_safe_people.R -\name{audit_safe_people} -\alias{audit_safe_people} -\alias{audit_safe_people.default} -\alias{audit_safe_people.opal} -\title{Audit Safe People details} +% Please edit documentation in R/audit_engine.R +\name{audit_engine} +\alias{audit_engine} +\alias{audit_engine.cr8tor} +\alias{audit_engine.opal} +\title{Audit Engine} \usage{ -audit_safe_people(x, ...) +audit_engine(x, ...) -\method{audit_safe_people}{default}(x, ...) +\method{audit_engine}{cr8tor}(x, ...) -\method{audit_safe_people}{opal}( +\method{audit_engine}{opal}( x, ..., - user, project = NULL, + user = NULL, logs_from = -Inf, logs_to = Inf, path = NULL @@ -22,14 +22,16 @@ audit_safe_people(x, ...) } \arguments{ \item{x}{This can be a connection to a 'DataSHIELD' server (e.g., object with -the \code{opal} class, see \code{\link[opalr:opal.login]{opalr::opal.login()}}).} +the \code{opal} class, see \code{\link[opalr:opal.login]{opalr::opal.login()}}). Alternatively, a governance +archive file, representing the intent of a project and associated +governance details.} \item{...}{Other optional arguments, see full documentation for details.} -\item{user}{String with the user name for which to extract Safe People +\item{project}{String with project name(s) from which to extra Safe Project details.} -\item{project}{String with project name(s) from which to extra Safe People +\item{user}{String with the user name for which to extract Safe People details.} \item{logs_from}{Lower limit timestamp to filter out the outputs generated @@ -43,9 +45,9 @@ be used to store log files. If not provided, logs will be stored within the RO-Crate returned by this function.} } \value{ -Updated RO-Crate object with Safe People information. +Audit RO-Crate with 5 Safes Components. } \description{ -Audit Safe People details from a 'DataSHIELD' server, an RO-Crate object or -a file path pointing to an RO-Crate. +Internal function to create audits for various back-ends. } +\keyword{internal} diff --git a/man/audit_safe_project.Rd b/man/audit_safe_project.Rd deleted file mode 100644 index 6b26c6c..0000000 --- a/man/audit_safe_project.Rd +++ /dev/null @@ -1,47 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/audit_safe_project.R -\name{audit_safe_project} -\alias{audit_safe_project} -\alias{audit_safe_project.default} -\alias{audit_safe_project.opal} -\title{Audit Safe Project details} -\usage{ -audit_safe_project(x, ...) - -\method{audit_safe_project}{default}(x, ...) - -\method{audit_safe_project}{opal}( - x, - ..., - project = NULL, - logs_from = -Inf, - logs_to = Inf, - path = NULL -) -} -\arguments{ -\item{x}{This can be a connection to a 'DataSHIELD' server (e.g., object with -the \code{opal} class, see \code{\link[opalr:opal.login]{opalr::opal.login()}}).} - -\item{...}{Other optional arguments, see full documentation for details.} - -\item{project}{String with project name(s) from which to extra Safe People -details.} - -\item{logs_from}{Lower limit timestamp to filter out the outputs generated -(default: \code{-Inf}, everything up to \code{logs_to})} - -\item{logs_to}{Upper limit timestamp to filter out the outputs generated -(default: \code{Inf}, everything from \code{logs_from} onwards).} - -\item{path}{String with path pointing to the root of the RO-Crate. This will -be used to store log files. If not provided, logs will be stored within -the RO-Crate returned by this function.} -} -\value{ -Updated RO-Crate object with Safe Project information. -} -\description{ -Audit Safe Project details from a 'DataSHIELD' server, an RO-Crate object or -a file path pointing to an RO-Crate. -} diff --git a/man/audit_study.Rd b/man/audit_study.Rd deleted file mode 100644 index b6efab8..0000000 --- a/man/audit_study.Rd +++ /dev/null @@ -1,57 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/audit_study.R -\name{audit_study} -\alias{audit_study} -\alias{audit_study.default} -\alias{audit_study.list} -\alias{audit_study.opal} -\title{Audit Study details} -\usage{ -audit_study(x, ...) - -\method{audit_study}{default}(x, ...) - -\method{audit_study}{list}( - x, - ..., - project = NULL, - logs_from = -Inf, - logs_to = Inf, - path = NULL -) - -\method{audit_study}{opal}( - x, - ..., - project = NULL, - logs_from = -Inf, - logs_to = Inf, - path = NULL -) -} -\arguments{ -\item{x}{This can be a connection to a 'DataSHIELD' server (e.g., object with -the \code{opal} class, see \code{\link[opalr:opal.login]{opalr::opal.login()}}).} - -\item{...}{Other optional arguments, see full documentation for details.} - -\item{project}{String with project name(s) from which to extra Safe People -details.} - -\item{logs_from}{Lower limit timestamp to filter out the outputs generated -(default: \code{-Inf}, everything up to \code{logs_to})} - -\item{logs_to}{Upper limit timestamp to filter out the outputs generated -(default: \code{Inf}, everything from \code{logs_from} onwards).} - -\item{path}{String with path pointing to the root of the RO-Crate. This will -be used to store log files. If not provided, logs will be stored within -the RO-Crate returned by this function.} -} -\value{ -Updated RO-Crate object with Study information. -} -\description{ -Audit Study details from a 'DataSHIELD' server, an RO-Crate object or -a file path pointing to an RO-Crate. -} diff --git a/man/dot-break_tibble.Rd b/man/dot-break_tibble.Rd deleted file mode 100644 index 984612d..0000000 --- a/man/dot-break_tibble.Rd +++ /dev/null @@ -1,20 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/rocrate_report.R -\name{.break_tibble} -\alias{.break_tibble} -\title{Break tibble by group, \code{varname}} -\usage{ -.break_tibble(df, varname) -} -\arguments{ -\item{df}{Data frame to be broken down into groups.} - -\item{varname}{String with variable name} -} -\value{ -String with data frame rendered with \code{kable}. -} -\description{ -Break tibble by group, \code{varname} -} -\keyword{internal} diff --git a/man/dot-markdown_report_body.Rd b/man/dot-markdown_report_body.Rd deleted file mode 100644 index fb89bdc..0000000 --- a/man/dot-markdown_report_body.Rd +++ /dev/null @@ -1,46 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/rocrate_report.R -\name{.markdown_report_body} -\alias{.markdown_report_body} -\title{Generate Markdown report's body} -\usage{ -.markdown_report_body( - report_contents, - overview_tbl, - safe_people_tbl, - safe_project_tbl, - safe_data_tbl, - safe_user_perm_tbl, - safe_setting_tbl, - safe_output_tbl, - break_by = NULL -) -} -\arguments{ -\item{report_contents}{String with Markdown report (e.g., header).} - -\item{overview_tbl}{Data frame with overview details for the RO-Crate.} - -\item{safe_people_tbl}{Data frame with Safe People details.} - -\item{safe_project_tbl}{Data frame with Safe Project details.} - -\item{safe_data_tbl}{Data frame with Safe Data details.} - -\item{safe_user_perm_tbl}{Data frame with Safe Data user permissions.} - -\item{safe_setting_tbl}{Data frame with Safe Setting details.} - -\item{safe_output_tbl}{Data frame with Safe Output details.} - -\item{break_by}{Optional string with variable to be used for breaking down -each table (e.g., server), as opposed to display all the results in a -single table.} -} -\value{ -String with updated Markdown report. -} -\description{ -Generate Markdown report's body -} -\keyword{internal} diff --git a/man/dot-markdown_report_header.Rd b/man/dot-markdown_report_header.Rd deleted file mode 100644 index 5d340c9..0000000 --- a/man/dot-markdown_report_header.Rd +++ /dev/null @@ -1,20 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/rocrate_report.R -\name{.markdown_report_header} -\alias{.markdown_report_header} -\title{Generate Markdown report's header} -\usage{ -.markdown_report_header(title, overview_tbl, diagram_filepath) -} -\arguments{ -\item{title}{String with title for the report (default: 'DataSHIELD Report').} - -\item{overview_tbl}{Data frame with overview details for the RO-Crate.} -} -\value{ -String with report's header -} -\description{ -Generate Markdown report's header -} -\keyword{internal} diff --git a/man/dot-markdown_report_rocrate.Rd b/man/dot-markdown_report_rocrate.Rd deleted file mode 100644 index 545990c..0000000 --- a/man/dot-markdown_report_rocrate.Rd +++ /dev/null @@ -1,28 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/rocrate_report.R -\name{.markdown_report_rocrate} -\alias{.markdown_report_rocrate} -\title{Embed RO-Crate in Markdown report} -\usage{ -.markdown_report_rocrate( - report_contents, - rocrate, - section_txt, - max_line_length = 200 -) -} -\arguments{ -\item{report_contents}{String with Markdown report (e.g., header).} - -\item{rocrate}{RO-Crate object (see \link[rocrateR:rocrate]{rocrateR::rocrate}).} - -\item{section_txt}{String with to be used as the section header -(e.g., RO-Crate).} -} -\value{ -String with update Markdown report. -} -\description{ -Embed RO-Crate in Markdown report -} -\keyword{internal} diff --git a/man/dot-overview_diagram.Rd b/man/dot-overview_diagram.Rd deleted file mode 100644 index b579631..0000000 --- a/man/dot-overview_diagram.Rd +++ /dev/null @@ -1,43 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/rocrate_report.R -\name{.overview_diagram} -\alias{.overview_diagram} -\title{Create diagram for RO-Crate overview} -\usage{ -.overview_diagram( - overview_tbl, - include_user_perm, - filepath, - render, - diag_title, - diag_width, - diag_height -) -} -\arguments{ -\item{overview_tbl}{Data frame with overview details for the RO-Crate.} - -\item{include_user_perm}{Boolean flag to indicate whether to include user -permissions in the report overview's diagram.} - -\item{filepath}{String with file path for Markdown report with the summary -of the given object, \code{x}.} - -\item{render}{Boolean flag to indicate whether to render the markdown report.} - -\item{diag_title}{String with title for the 'root' of the diagram (default: -'DataSHIELD server').} - -\item{diag_width}{Numeric value with width (in inches) for the report -overview's diagram (default: \code{NULL}, estimated based on number of nodes).} - -\item{diag_height}{Numeric value with height (in inches) for the report -overview's diagram (default: \code{NULL}, estimated based on number of nodes).} -} -\value{ -Diagram object -} -\description{ -Create diagram for RO-Crate overview -} -\keyword{internal} diff --git a/man/dot-tidy_overview.Rd b/man/dot-tidy_overview.Rd deleted file mode 100644 index 405b0ad..0000000 --- a/man/dot-tidy_overview.Rd +++ /dev/null @@ -1,21 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/rocrate_report.R -\name{.tidy_overview} -\alias{.tidy_overview} -\title{Create tidy version of the overview table} -\usage{ -.tidy_overview(overview_tbl, include_user_perm) -} -\arguments{ -\item{overview_tbl}{Data frame with overview details for the RO-Crate.} - -\item{include_user_perm}{Boolean flag to indicate whether to include user -permissions in the report overview's diagram.} -} -\value{ -Data frame with tidy overview table. -} -\description{ -Create tidy version of the overview table -} -\keyword{internal} diff --git a/man/dsROCrate-package.Rd b/man/dsROCrate-package.Rd index f504919..5e69fe9 100644 --- a/man/dsROCrate-package.Rd +++ b/man/dsROCrate-package.Rd @@ -4,18 +4,26 @@ \name{dsROCrate-package} \alias{dsROCrate} \alias{dsROCrate-package} -\title{dsROCrate: 'DataSHIELD' RO-Crate Wrapper Functions} +\title{dsROCrate: 'DataSHIELD' RO-Crate Governance Functions} \description{ \if{html}{\figure{logo.png}{options: style='float: right' alt='logo' width='120'}} -R package to wrap elements from 'DataSHIELD' analysis into an RO-Crate. +Tools for wrapping 'DataSHIELD' analyses into RO-Crate (Research Object Crate) objects. Provides functions to create structured metadata for federated data analysis projects, enabling governance tracking of data access, project membership, analysis execution and output validation across distributed data sources. +} +\seealso{ +Useful links: +\itemize{ + \item \url{https://github.com/FederatedMethods/dsROCrate} + \item Report bugs at \url{https://github.com/FederatedMethods/dsROCrate/issues} +} + } \author{ \strong{Maintainer}: Roberto Villegas-Diaz \email{r.villegas-diaz@outlook.com} (\href{https://orcid.org/0000-0001-5036-8661}{ORCID}) Authors: \itemize{ - \item Rebecca Wilson (\href{https://orcid.org/0000-0003-2294-593X}{ORCID}) + \item Becca Wilson (\href{https://orcid.org/0000-0003-2294-593X}{ORCID}) \item Olly Butters (\href{https://orcid.org/0000-0003-0354-8461}{ORCID}) \item Stuart Wheater (\href{https://orcid.org/0009-0003-2419-1964}{ORCID}) } diff --git a/man/extract_safe_data.Rd b/man/extract_safe_data.Rd index 9674262..32853fb 100644 --- a/man/extract_safe_data.Rd +++ b/man/extract_safe_data.Rd @@ -10,7 +10,13 @@ extract_safe_data(x, ...) \method{extract_safe_data}{opal}(x, ..., rocrate = rocrateR::rocrate_5s()) -\method{extract_safe_data}{rocrate}(x, ..., id = NULL, rocrate = rocrateR::rocrate_5s()) +\method{extract_safe_data}{rocrate}( + x, + ..., + id = NULL, + asset_id_suffix = "#asset:", + rocrate = rocrateR::rocrate_5s() +) } \arguments{ \item{x}{This can be a connection to a 'DataSHIELD' server (e.g., object with @@ -24,6 +30,9 @@ RO-Crate.} \item{id}{(Optional) Vector with \verb{@id} strings for Safe Data entity(ies) to be extracted from the given RO-Crate, \code{x}.} + +\item{asset_id_suffix}{String with ID suffix for the tables/datasets +entities in the RO-Crate (default: \code{"#asset:"}).} } \value{ RO-Crate with Safe Data entity(ies). diff --git a/man/figures/README-safe_people_crate_audit_v1-1.png b/man/figures/README-safe_people_crate_audit_v1-1.png deleted file mode 100644 index 758c424..0000000 Binary files a/man/figures/README-safe_people_crate_audit_v1-1.png and /dev/null differ diff --git a/man/figures/README-safe_people_crate_audit_v2-1.png b/man/figures/README-safe_people_crate_audit_v2-1.png deleted file mode 100644 index bf5701a..0000000 Binary files a/man/figures/README-safe_people_crate_audit_v2-1.png and /dev/null differ diff --git a/man/figures/README-safe_project_crate_audit_v1-1.png b/man/figures/README-safe_project_crate_audit_v1-1.png deleted file mode 100644 index 758c424..0000000 Binary files a/man/figures/README-safe_project_crate_audit_v1-1.png and /dev/null differ diff --git a/man/figures/README-study_crate_audit_v1-1.png b/man/figures/README-study_crate_audit_v1-1.png deleted file mode 100644 index 4c6a33b..0000000 Binary files a/man/figures/README-study_crate_audit_v1-1.png and /dev/null differ diff --git a/man/figures/logo.png b/man/figures/logo.png index 2aedf15..3952ea5 100644 Binary files a/man/figures/logo.png and b/man/figures/logo.png differ diff --git a/man/figures/logo_black.png b/man/figures/logo_black.png index bab0360..c115445 100644 Binary files a/man/figures/logo_black.png and b/man/figures/logo_black.png differ diff --git a/man/figures/logo_white.png b/man/figures/logo_white.png index ccde747..f706d72 100644 Binary files a/man/figures/logo_white.png and b/man/figures/logo_white.png differ diff --git a/man/flatten_safe_data.Rd b/man/flatten_safe_data.Rd index 1dff1fe..1ae4c16 100644 --- a/man/flatten_safe_data.Rd +++ b/man/flatten_safe_data.Rd @@ -10,7 +10,7 @@ flatten_safe_data(x, ...) \method{flatten_safe_data}{default}(x, ...) -\method{flatten_safe_data}{rocrate}(x, ..., id = NULL) +\method{flatten_safe_data}{rocrate}(x, ..., id = NULL, asset_id_suffix = "#asset:") } \arguments{ \item{x}{Object (e.g., RO-Crate) with Safe Data details. This can be @@ -20,7 +20,7 @@ generated with the \code{\link[=extract_safe_data]{extract_safe_data()}} functio If not provided, extract all entities with \verb{@type = 'Dataset'}.} } \value{ -Data frame with safe data details. +Data frame with Safe Data details. } \description{ Flatten object with Safe Data details diff --git a/man/init.Rd b/man/init.Rd index 6151dc8..646685f 100644 --- a/man/init.Rd +++ b/man/init.Rd @@ -4,7 +4,7 @@ \alias{init} \alias{init.opal} \alias{init.rocrate} -\title{Initialise Five Safes RO-Crate} +\title{Initialise a Five Safes RO-Crate} \usage{ init(x, ...) @@ -70,8 +70,13 @@ values will be extracted from (e.g., OBiBa's Opal). Optional, if \code{x} is set to a connection object. If so, then \code{rocrate} is required.} } \value{ -Five Safes RO-Crate. +Five Safes RO-Crate object. } \description{ -Initialise Five Safes RO-Crate +Creates a new RO-Crate configured for Five Safes auditing. +} +\references{ +Wilkinson, M., Dumontier, M., Aalbersberg, I. et al. (2016) The FAIR Guiding +Principles for scientific data management and stewardship. Sci Data 3, +160018. https://doi.org/10.1038/sdata.2016.18 } diff --git a/man/load_cr8tor_bundle.Rd b/man/load_cr8tor_bundle.Rd index 80953e6..8378d49 100644 --- a/man/load_cr8tor_bundle.Rd +++ b/man/load_cr8tor_bundle.Rd @@ -4,10 +4,10 @@ \alias{load_cr8tor_bundle} \title{Load cr8tor governance bundle / project archive} \usage{ -load_cr8tor_bundle(path, ...) +load_cr8tor_bundle(x, ...) } \arguments{ -\item{path}{Path to cr8tor ZIP archive.} +\item{x}{Path to cr8tor ZIP archive.} \item{...}{Unused.} } @@ -22,3 +22,4 @@ A cr8tor project archive contains: \item config.toml → platform configuration. } } +\keyword{internal} diff --git a/man/load_rocrate.Rd b/man/load_rocrate.Rd deleted file mode 100644 index 1d2f9c8..0000000 --- a/man/load_rocrate.Rd +++ /dev/null @@ -1,18 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils-rocrate.R -\name{load_rocrate} -\alias{load_rocrate} -\title{Load RO-Crate from file} -\usage{ -load_rocrate(x) -} -\arguments{ -\item{x}{String with path to RO-Crate} -} -\value{ -RO-Crate object. -} -\description{ -Load RO-Crate from file -} -\keyword{internal} diff --git a/man/parse_user_profiles.Rd b/man/parse_user_profiles.Rd index d34dbb8..2611227 100644 --- a/man/parse_user_profiles.Rd +++ b/man/parse_user_profiles.Rd @@ -4,11 +4,14 @@ \alias{parse_user_profiles} \alias{parse_user_profiles,armadillo-method} \alias{parse_user_profiles.opal} +\alias{parse_user_profiles.ArmadilloCredentials} \title{Parse user profiles} \usage{ \S4method{parse_user_profiles}{armadillo}(x, ..., user) -parse_user_profiles.opal(x, ..., user) +\method{parse_user_profiles}{opal}(x, ..., user) + +\method{parse_user_profiles}{ArmadilloCredentials}(x, ..., user) } \arguments{ \item{x}{Connection object to backend for DataSHIELD server (e.g., Opal).} diff --git a/man/project_exists.Rd b/man/project_exists.Rd index 5dfa49a..f3d537f 100644 --- a/man/project_exists.Rd +++ b/man/project_exists.Rd @@ -4,6 +4,7 @@ \alias{project_exists} \alias{project_exists,armadillo-method} \alias{project_exists.opal} +\alias{project_exists.ArmadilloCredentials} \title{Verify if project exists} \usage{ \S4method{project_exists}{armadillo}( @@ -12,7 +13,9 @@ project ) -project_exists.opal(x, ..., project) +\method{project_exists}{opal}(x, ..., project) + +\method{project_exists}{ArmadilloCredentials}(x, ..., project) } \arguments{ \item{x}{Connection object to backend for DataSHIELD server (e.g., Opal).} @@ -38,6 +41,9 @@ Other Opal: \code{\link{get_project_tables}()}, \code{\link{get_table_permissions}()}, \code{\link{parse_user_profiles}()} + +Other Armadillo: +\code{\link{parse_user_profiles}()} } \concept{Armadillo} \concept{Opal} diff --git a/man/rocrate_report.Rd b/man/report.Rd similarity index 84% rename from man/rocrate_report.Rd rename to man/report.Rd index 5fb57da..fcfa194 100644 --- a/man/rocrate_report.Rd +++ b/man/report.Rd @@ -1,16 +1,16 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/rocrate_report.R -\name{rocrate_report} -\alias{rocrate_report} -\alias{rocrate_report.character} -\alias{rocrate_report.default} -\alias{rocrate_report.list} -\alias{rocrate_report.rocrate} +% Please edit documentation in R/report.R +\name{report} +\alias{report} +\alias{report.character} +\alias{report.default} +\alias{report.list} +\alias{report.rocrate} \title{Create an RO-Crate report} \usage{ -rocrate_report(x, ...) +report(x, ...) -\method{rocrate_report}{character}( +\method{report}{character}( x, ..., title = "DataSHIELD Report", @@ -25,9 +25,9 @@ rocrate_report(x, ...) max_line_length = 200 ) -\method{rocrate_report}{default}(x, ...) +\method{report}{default}(x, ...) -\method{rocrate_report}{list}( +\method{report}{list}( x, ..., study_name, @@ -43,7 +43,7 @@ rocrate_report(x, ...) max_line_length = 200 ) -\method{rocrate_report}{rocrate}( +\method{report}{rocrate}( x, ..., title = "DataSHIELD Report", @@ -63,7 +63,7 @@ rocrate_report(x, ...) string with the path to an RO-Crate.} \item{...}{Other optional arguments. See the full documentation, -\code{\link[=rocrate_report]{?dsROCrate::rocrate_report}}.} +\code{\link[=report]{?dsROCrate::report}}.} \item{title}{String with title for the report (default: 'DataSHIELD Report').} diff --git a/man/safe_project.Rd b/man/safe_project.Rd index a951b48..3321e50 100644 --- a/man/safe_project.Rd +++ b/man/safe_project.Rd @@ -6,6 +6,7 @@ \alias{safe_project.character} \alias{safe_project.opal} \alias{safe_project.rocrate} +\alias{safe_project.ArmadilloCredentials} \title{Safe Project details} \source{ \itemize{ @@ -69,6 +70,20 @@ tables = attr(x, "tables"), user = attr(x, "user") ) + +\method{safe_project}{ArmadilloCredentials}( + x, + ..., + profile = "default", + project = NULL, + rocrate = rocrateR::rocrate_5s(), + asset_id_suffix = "#asset:", + project_id_suffix = "#project:", + path = NULL, + resources = NULL, + tables = NULL, + user = NULL +) } \arguments{ \item{x}{This can be a connection to a 'DataSHIELD' server (e.g., object with diff --git a/man/user_perm_entity.Rd b/man/user_perm_entity.Rd index 87d6e04..fac1c4c 100644 --- a/man/user_perm_entity.Rd +++ b/man/user_perm_entity.Rd @@ -2,18 +2,18 @@ % Please edit documentation in R/utils-opal.R \name{user_perm_entity} \alias{user_perm_entity} -\title{Create user permission entities} +\title{Create user/person permission entities} \usage{ -user_perm_entity(user, user_id, table, table_id, permission, ...) +user_perm_entity(person, person_id, asset, asset_id, permission, ...) } \arguments{ -\item{user}{String with user name.} +\item{person}{String with person name/username.} -\item{user_id}{String with user \verb{@id}.} +\item{person_id}{String with person \verb{@id}.} -\item{table}{String with dataset/table name.} +\item{asset}{String with dataset/table/resource name.} -\item{table_id}{String with dataset/table \verb{@id}.} +\item{asset_id}{String with dataset/table \verb{@id}.} \item{permission}{String with permission ('view', 'view-values', 'edit', 'edit-values' OR 'administrate').} @@ -24,6 +24,6 @@ user_perm_entity(user, user_id, table, table_id, permission, ...) List of \link[rocrateR:entity]{rocrateR::entity} objects } \description{ -Create user permission entities +Create user/person permission entities } \keyword{internal} diff --git a/tests/testthat/test-audit_safe_people.R b/tests/testthat/test-audit_safe_people.R deleted file mode 100644 index 07baf89..0000000 --- a/tests/testthat/test-audit_safe_people.R +++ /dev/null @@ -1,88 +0,0 @@ -test_that("default method errors for unsupported classes", { - expect_error( - audit_safe_people(123), - "Unknown class" - ) -}) - -test_that("rocrate method fails for invalid object", { - expect_error( - audit_safe_people(list()), - class = "error" - ) -}) - -test_that("opal method returns RO-Crate with expected attributes", { - # setup - opal_con <- opal_demo_con() - - # ignore warning about empty logs - suppressWarnings( - crate <- audit_safe_people(opal_con, user = attr(opal_con, "PEOPLE")) - ) - - expect_s3_class(crate, "rocrate") - - expect_equal(attr(crate, "audit_type"), "Safe People") - expect_true("project" %in% names(attributes(crate))) - - # close connection to OBiBa's Opal demo server - opalr::opal.logout(opal_con) -}) - -test_that("opal method works with specific project", { - # setup - opal_con <- opal_demo_con() - - # ignore warning about empty logs - suppressWarnings( - crate <- audit_safe_people( - opal_con, - project = attr(opal_con, "PROJECT"), - user = attr(opal_con, "PEOPLE") - ) - ) - - expect_s3_class(crate, "rocrate") - expect_equal(attr(crate, "project"), attr(opal_con, "PROJECT")) - - # close connection to OBiBa's Opal demo server - opalr::opal.logout(opal_con) - - # close connection to OBiBa's Opal demo server - opalr::opal.logout(opal_con) -}) - -test_that("opal method errors for unknown project", { - # setup - opal_con <- opal_demo_con() - - expect_error( - audit_safe_people( - opal_con, - project = "NON_EXISTENT_PROJECT", - user = attr(opal_con, "PEOPLE") - ), - "The given `project`, does not have any permissions set for the given `user`!", - fixed = TRUE - ) -}) - -test_that("path argument is stored as attribute", { - # setup - opal_con <- opal_demo_con() - - # ignore warning about empty logs - suppressWarnings( - crate <- audit_safe_people( - opal_con, - path = tempdir(), - user = attr(opal_con, "PEOPLE") - ) - ) - - expect_equal(attr(crate, "path"), tempdir()) - - # close connection to OBiBa's Opal demo server - opalr::opal.logout(opal_con) -}) diff --git a/tests/testthat/test-audit_safe_project.R b/tests/testthat/test-audit_safe_project.R deleted file mode 100644 index a7b868b..0000000 --- a/tests/testthat/test-audit_safe_project.R +++ /dev/null @@ -1,82 +0,0 @@ -test_that("default method errors for unsupported classes", { - expect_error( - audit_safe_project(123), - "Unknown class" - ) -}) - -test_that("rocrate method fails for invalid object", { - expect_error( - audit_safe_project(list()), - class = "error" - ) -}) - -test_that("opal method returns RO-Crate with expected attributes", { - # setup - opal_con <- opal_demo_con() - - # ignore warning about empty logs - suppressWarnings( - crate <- audit_safe_project(opal_con) - ) - - expect_s3_class(crate, "rocrate") - - expect_equal(attr(crate, "audit_type"), "Safe Project") - expect_true("project" %in% names(attributes(crate))) - - # close connection to OBiBa's Opal demo server - opalr::opal.logout(opal_con) -}) - -test_that("opal method works with specific project", { - # setup - opal_con <- opal_demo_con() - - # ignore warning about empty logs - suppressWarnings( - crate <- audit_safe_project( - opal_con, - project = attr(opal_con, "PROJECT") - ) - ) - - expect_s3_class(crate, "rocrate") - expect_equal(attr(crate, "project"), attr(opal_con, "PROJECT")) - - # close connection to OBiBa's Opal demo server - opalr::opal.logout(opal_con) -}) - -test_that("opal method errors for unknown project", { - # setup - opal_con <- opal_demo_con() - - expect_error( - audit_safe_project(opal_con, project = "NON_EXISTENT_PROJECT"), - "No data details were found", - fixed = TRUE - ) - - # close connection to OBiBa's Opal demo server - opalr::opal.logout(opal_con) -}) - -test_that("path argument is stored as attribute", { - # setup - opal_con <- opal_demo_con() - - # ignore warning about empty logs - suppressWarnings( - crate <- audit_safe_project( - opal_con, - path = tempdir() - ) - ) - - expect_equal(attr(crate, "path"), tempdir()) - - # close connection to OBiBa's Opal demo server - opalr::opal.logout(opal_con) -}) diff --git a/tests/testthat/test-audit_study.R b/tests/testthat/test-audit_study.R deleted file mode 100644 index 24cac16..0000000 --- a/tests/testthat/test-audit_study.R +++ /dev/null @@ -1,89 +0,0 @@ -test_that("default method errors for unsupported classes", { - expect_error( - audit_study(123), - "Unknown class" - ) -}) - -test_that("list method validates a list object with", { - expect_no_error( - audit_study(list()) - ) -}) - -test_that("list method fails for invalid object", { - # setup - crate <- rocrateR::rocrate_5s() - - expect_error( - audit_study(crate), - class = "error" - ) -}) - -test_that("list method returns list with expected attributes", { - # setup - opal_con <- opal_demo_con() - - # ignore warning about empty logs - suppressWarnings( - crate <- audit_study(list(demo = opal_con)) - ) - - expect_equal(attr(crate, "audit_type"), "Study") - expect_true("demo" %in% names(crate)) - expect_true(length(crate) == 1) - - # close connection to OBiBa's Opal demo server - opalr::opal.logout(opal_con) -}) - -test_that("list method works with specific project", { - # setup - opal_con <- opal_demo_con() - - # ignore warning about empty logs - suppressWarnings( - crate <- audit_study( - list(demo = opal_con), - project = attr(opal_con, "PROJECT") - ) - ) - - expect_equal(attr(crate, "project"), attr(opal_con, "PROJECT")) - - # close connection to OBiBa's Opal demo server - opalr::opal.logout(opal_con) -}) - -test_that("list method errors for unknown project", { - # setup - opal_con <- opal_demo_con() - - expect_error( - audit_study(list(demo = opal_con), project = "NON_EXISTENT_PROJECT"), - "No data details were found", - fixed = TRUE - ) - - # close connection to OBiBa's Opal demo server - opalr::opal.logout(opal_con) -}) - -test_that("path argument is stored as attribute", { - # setup - opal_con <- opal_demo_con() - - # ignore warning about empty logs - suppressWarnings( - crate <- audit_study( - list(demo = opal_con), - path = tempdir() - ) - ) - - expect_equal(attr(crate, "path"), tempdir()) - - # close connection to OBiBa's Opal demo server - opalr::opal.logout(opal_con) -}) diff --git a/tests/testthat/test-dsROCrate-package.R b/tests/testthat/test-dsROCrate-package.R new file mode 100644 index 0000000..03e64d9 --- /dev/null +++ b/tests/testthat/test-dsROCrate-package.R @@ -0,0 +1,19 @@ +test_that("All Rd files have a value section", { + db <- tools::Rd_db("dsROCrate") + + # drop the package-level Rd (e.g., "dsROCrate-package") + db <- db[!grepl("-package\\.Rd$", names(db))] + + has_value <- function(rd) { + any(vapply(rd, function(x) attr(x, "Rd_tag") == "\\value", logical(1))) + } + + results <- vapply(db, has_value, logical(1)) + missing <- names(db)[!results] + + expect_equal( + length(missing), + 0L, + info = paste("Missing \\value in:\n", paste(" -", missing, collapse = "\n")) + ) +}) diff --git a/tests/testthat/test-rocrate_report.R b/tests/testthat/test-report.R similarity index 75% rename from tests/testthat/test-rocrate_report.R rename to tests/testthat/test-report.R index afdcd0c..551277f 100644 --- a/tests/testthat/test-rocrate_report.R +++ b/tests/testthat/test-report.R @@ -1,18 +1,18 @@ -test_that("rocrate_report.default errors", { +test_that("report.default errors", { expect_error( - rocrate_report(123), + report(123), "Unknown class" ) }) -test_that("rocrate_report.character errors for invalid path", { +test_that("report.character errors for invalid path", { expect_error( - rocrate_report("nonexistent_path", render = FALSE), - "is not a valid path" + report("nonexistent_path", render = FALSE), + "The provided path does not exist" ) }) -test_that("rocrate_report.character errors if read_rocrate fails", { +test_that("report.character errors if read_rocrate fails", { # create temporary file tmp_dir <- file.path(tempdir(), "dsROCRate_tests") tmp_file <- tempfile(tmpdir = tmp_dir) @@ -24,15 +24,15 @@ test_that("rocrate_report.character errors if read_rocrate fails", { read_rocrate = function(...) stop("fail"), code = { expect_error( - rocrate_report(tmp_file, render = FALSE), - "Unable to load an RO-Crate" + report(tmp_file, render = FALSE), + "Could not determine how to load RO-Crate from provided input" ) }, .package = "rocrateR" ) }) -test_that("rocrate_report.character errors if input is invalid .zip", { +test_that("report.character errors if input is invalid .zip", { # create temporary file tmp_dir <- file.path(tempdir(), "dsROCRate_tests") tmp_file <- tempfile(tmpdir = tmp_dir, fileext = ".zip") @@ -40,10 +40,10 @@ test_that("rocrate_report.character errors if input is invalid .zip", { on.exit(unlink(tmp_dir, recursive = TRUE, force = TRUE)) file.create(tmp_file) - expect_error(rocrate_report(tmp_file, render = FALSE)) + expect_error(report(tmp_file, render = FALSE)) }) -test_that("rocrate_report.character dispatches to rocrate method", { +test_that("report.character dispatches to rocrate method", { # create temporary file tmp_dir <- file.path(tempdir(), "dsROCRate_tests") dir.create(tmp_dir, recursive = TRUE) @@ -59,16 +59,16 @@ test_that("rocrate_report.character dispatches to rocrate method", { ) testthat::with_mocked_bindings( load_content = function(x, ...) x, - rocrate_report.rocrate = function(x, ...) "OK", + report.character = function(x, ...) "OK", code = { - result <- rocrate_report(tmp_dir, render = FALSE) + result <- report(tmp_dir, render = FALSE) expect_equal(result, "OK") }, .package = "dsROCrate" ) }) -test_that("rocrate_report.rocrate errors if required entities missing", { +test_that("report.rocrate errors if required entities missing", { fake_rocrate <- structure(list(), class = "rocrate") testthat::local_mocked_bindings( @@ -82,7 +82,7 @@ test_that("rocrate_report.rocrate errors if required entities missing", { extract_safe_data = function(...) NULL, code = { expect_error( - rocrate_report(fake_rocrate, render = FALSE), + report(fake_rocrate, render = FALSE), "missing" ) }, @@ -90,7 +90,7 @@ test_that("rocrate_report.rocrate errors if required entities missing", { ) }) -test_that("rocrate_report.rocrate returns expected structure", { +test_that("report.rocrate returns expected structure", { fake_rocrate <- structure(list(), class = "rocrate") safe_people <- list() @@ -109,12 +109,19 @@ test_that("rocrate_report.rocrate returns expected structure", { extract_safe_setting = function(...) NULL, extract_safe_output = function(...) NULL, flatten_safe_people = function(...) { - tibble::tibble(id = "u1", name = "dsuser", project = "P1") + tibble::tibble(person_id = "u1", name = "dsuser", project = "P1") }, flatten_safe_project = function(...) { - tibble::tibble(id = "p1", project = "P1", table = "T1") + tibble::tibble( + project_id = "p1", + project = "P1", + asset_id = "t1", + asset = "T1" + ) + }, + flatten_safe_data = function(...) { + tibble::tibble(asset_id = "t1", asset = "T1") }, - flatten_safe_data = function(...) tibble::tibble(id = "t1", name = "T1"), flatten_safe_output = function(...) tibble::tibble(), flatten_user_perm_entity = function(...) NULL, .overview_diagram = function(...) list(diag_lst = "diag", diag_path = NULL), @@ -123,7 +130,7 @@ test_that("rocrate_report.rocrate returns expected structure", { .markdown_report_body = function(...) "BODY", .markdown_report_rocrate = function(...) "ROCRATE", code = { - result <- rocrate_report( + result <- report( fake_rocrate, render = FALSE, filepath = tempfile(fileext = ".md") @@ -148,7 +155,7 @@ test_that("rocrate_report.rocrate returns expected structure", { ) }) -test_that("rocrate_report.rocrate prevents overwrite", { +test_that("report.rocrate prevents overwrite", { fake_rocrate <- structure(list(), class = "rocrate") # create temporary file tmp_dir <- file.path(tempdir(), "dsROCRate_tests") @@ -168,7 +175,7 @@ test_that("rocrate_report.rocrate prevents overwrite", { extract_safe_data = function(...) list(), code = { expect_error( - rocrate_report(fake_rocrate, filepath = tmp_file, overwrite = FALSE), + report(fake_rocrate, filepath = tmp_file, overwrite = FALSE), "existing file" ) }, @@ -176,20 +183,25 @@ test_that("rocrate_report.rocrate prevents overwrite", { ) }) -test_that("rocrate_report.list aggregates multiple servers", { +test_that("report.list aggregates multiple servers", { fake_rocrate <- structure(list(), class = "rocrate") server_output <- list( - safe_people = tibble::tibble(id = "u1", name = "dsuser"), - safe_project = tibble::tibble(id = "p1", table = "T1"), - safe_data = tibble::tibble(id = "t1", name = "T1"), + safe_people = tibble::tibble(id_person = "u1", name = "dsuser"), + safe_project = tibble::tibble( + id_project = "p1", + project = "P1", + asset_id = "t1", + asset = "T1" + ), + safe_data = tibble::tibble(asset_id = "t1", asset = "T1"), safe_data_permissions = NULL, safe_setting = tibble::tibble(), safe_output = tibble::tibble(), overview_data = tibble::tibble( project = "P1", - table = "T1", - user = "dsuser" + asset = "T1", + person = "dsuser" ) ) @@ -199,14 +211,14 @@ test_that("rocrate_report.list aggregates multiple servers", { ) testthat::with_mocked_bindings( - rocrate_report = function(...) server_output, + report = function(...) server_output, .overview_diagram = function(...) list(diag_lst = "diag", diag_path = NULL), .tidy_overview = function(...) tibble::tibble(Project = "P1"), .markdown_report_header = function(...) "HEADER", .markdown_report_body = function(...) "BODY", .markdown_report_rocrate = function(...) "ROCRATE", code = { - result <- rocrate_report( + result <- report( list(server1 = fake_rocrate, server2 = fake_rocrate), study_name = "StudyX", render = FALSE, @@ -223,7 +235,7 @@ test_that("rocrate_report.list aggregates multiple servers", { test_that(".tidy_overview collapses permissions correctly", { df <- tibble::tibble( project = "P1", - table = "T1", + asset = "T1", name = "dsuser", permission = c("read", "write") ) @@ -254,13 +266,13 @@ test_that(".break_tibble returns input table when varname = NULL", { expect_equal(knitr::kable(df), .break_tibble(df, NULL)) }) -test_that("rocrate_report fails if the given path points to a non-existing directory", { +test_that("report fails if the given path points to a non-existing directory", { expect_error( - rocrate_report(rocrateR::rocrate_5s(), filepath = "path/to/dir") + report(rocrateR::rocrate_5s(), filepath = "path/to/dir") ) }) -test_that("rocrate_report works end-to-end with real dsROCrate audit outputs", { +test_that("report works end-to-end with real dsROCrate audit outputs", { # create temporary file tmp_dir <- file.path(tempdir(), "dsROCRate_tests") dir.create(tmp_dir, recursive = TRUE) @@ -270,17 +282,20 @@ test_that("rocrate_report works end-to-end with real dsROCrate audit outputs", { opal_con <- opal_demo_con() # generate audit RO-Crate for a Safe Project - roc <- audit_safe_project( - opal_con, - project = "CNSIM", - path = tmp_dir - ) + # suppress warnings about missing logs + suppressWarnings({ + roc <- audit( + opal_con, + project = "CNSIM", + path = tmp_dir + ) + }) out_file <- file.path(tmp_dir, "report.md") # ignore warnings about missing permission entities (e.g., @type = 'ControlAction') suppressWarnings( - result <- rocrate_report( + result <- report( roc, filepath = out_file, render = FALSE, @@ -300,7 +315,7 @@ test_that("rocrate_report works end-to-end with real dsROCrate audit outputs", { opalr::opal.logout(opal_con) }) -test_that("rocrate_report.character loads crate from disk", { +test_that("report.character loads crate from disk", { # create temporary file tmp_dir <- file.path(tempdir(), "dsROCRate_tests") dir.create(tmp_dir, recursive = TRUE) @@ -310,11 +325,14 @@ test_that("rocrate_report.character loads crate from disk", { opal_con <- opal_demo_con() # generate audit RO-Crate for a Safe Project - roc <- audit_safe_project( - opal_con, - project = "CNSIM", - path = tmp_dir - ) + # suppress warnings about missing logs + suppressWarnings({ + roc <- audit( + opal_con, + project = "CNSIM", + path = tmp_dir + ) + }) # write crate to disk (real metadata file) path_to_rocrate_bag <- rocrateR::bag_rocrate(roc, path = tmp_dir) @@ -323,7 +341,7 @@ test_that("rocrate_report.character loads crate from disk", { # ignore warnings about missing permission entities (e.g., @type = 'ControlAction') suppressWarnings( - result <- rocrate_report( + result <- report( path_to_rocrate_bag, filepath = out_file, render = FALSE, @@ -339,7 +357,7 @@ test_that("rocrate_report.character loads crate from disk", { opalr::opal.logout(opal_con) }) -test_that("rocrate_report.list aggregates outputs from a study audit", { +test_that("report.list aggregates outputs from a study audit", { # create temporary file tmp_dir <- file.path(tempdir(), "dsROCRate_tests") dir.create(tmp_dir, recursive = TRUE) @@ -349,17 +367,20 @@ test_that("rocrate_report.list aggregates outputs from a study audit", { opal_con <- opal_demo_con() # generate audit RO-Crate for a study - roc <- audit_study( - list(demo = opal_con), - project = "CNSIM", - path = tmp_dir + ## ignore warnings about missing logs + suppressWarnings( + roc <- audit( + list(demo = opal_con), + project = "CNSIM", + path = tmp_dir + ) ) out_file <- file.path(tmp_dir, "report.md") # ignore warnings about missing permission entities (e.g., @type = 'ControlAction') suppressWarnings( - result <- rocrate_report( + result <- report( roc, filepath = out_file, study_name = "StudyX", @@ -376,11 +397,11 @@ test_that("rocrate_report.list aggregates outputs from a study audit", { opalr::opal.logout(opal_con) }) -test_that("rocrate_report.list handles missing outputs from a study audit", { +test_that("report.list handles missing outputs from a study audit", { testthat::with_mocked_bindings( - rocrate_report = function(...) list(), + report = function(...) list(), code = { - result <- rocrate_report( + result <- report( list(server1 = rocrateR::rocrate_5s()), study_name = "StudyX", render = FALSE, diff --git a/tests/testthat/test-safe-people.R b/tests/testthat/test-safe-people.R index b98c9cb..c8ecdae 100644 --- a/tests/testthat/test-safe-people.R +++ b/tests/testthat/test-safe-people.R @@ -7,25 +7,6 @@ test_that("safe_people fails when attempt calling function with invalid class", ) }) -test_that("safe_people fails when attempt adding user to RO-Crate without project entity, `@type = 'Project`", { - # # setup - # ## open connection to OBiBa's Opal demo server - # opal_con <- opal_demo_con() - # ## create basic RO-Crate - # basic_rocrate <- rocrateR::rocrate_5s() - # - # expect_warning( - # opal_con |> - # dsROCrate::safe_people( - # rocrate = basic_rocrate, - # user = attr(opal_con, "PEOPLE") - # ) - # ) - # - # # close connection to OBiBa's Opal demo server - # opalr::opal.logout(opal_con) -}) - test_that("safe_people fails when attempt calling with invalid connection", { # setup ## open connection to OBiBa's Opal demo server diff --git a/tests/testthat/test-safe_output.R b/tests/testthat/test-safe_output.R index 3fa72b4..8d6f04d 100644 --- a/tests/testthat/test-safe_output.R +++ b/tests/testthat/test-safe_output.R @@ -183,14 +183,17 @@ test_that("safe_output works", { tempdir_name <- tempdir() on.exit(unlink(tempdir_name, force = TRUE, recursive = TRUE)) expect_true(dir.exists(tempdir_name)) - basic_rocrate_8 <- basic_rocrate_3 |> - dsROCrate::safe_output( - connection = opal_con, - logs_from = Sys.time() - 60, # capture the last min - logs_to = Sys.time(), - user = "dsuser", - path = tempdir_name - ) + ## ignore warnings about missing logs + suppressWarnings( + basic_rocrate_8 <- basic_rocrate_3 |> + dsROCrate::safe_output( + connection = opal_con, + logs_from = Sys.time() - 60, # capture the last min + logs_to = Sys.time(), + user = "dsuser", + path = tempdir_name + ) + ) unlink(tempdir_name, force = TRUE, recursive = TRUE) expect_false(dir.exists(tempdir_name)) diff --git a/tests/testthat/test-utils-connection.R b/tests/testthat/test-utils-connection.R index 0f72e69..69e793e 100644 --- a/tests/testthat/test-utils-connection.R +++ b/tests/testthat/test-utils-connection.R @@ -35,25 +35,25 @@ test_that("project_exists() dispatches to opal method", { opalr::opal.logout(opal_con) }) -test_that("project_exists() works for armadillo objects", { - skip_if_not_installed("MolgenisArmadillo") - - arm <- methods::new("ArmadilloCredentials") - - local_mocked_bindings( - armadillo.list_projects = function() c("proj1", "proj2"), - .package = "MolgenisArmadillo" - ) - - expect_silent( - project_exists(arm, project = "proj1") - ) - - expect_error( - project_exists(arm, project = "missing"), - "was not found in the given Armadillo connection!" - ) -}) +# test_that("project_exists() works for armadillo objects", { +# skip_if_not_installed("MolgenisArmadillo") +# +# arm <- methods::new("ArmadilloCredentials") +# +# local_mocked_bindings( +# armadillo.list_projects = function() c("proj1", "proj2"), +# .package = "MolgenisArmadillo" +# ) +# +# expect_silent( +# project_exists(arm, project = "proj1") +# ) +# +# expect_error( +# project_exists(arm, project = "missing"), +# "was not found in the given Armadillo connection!" +# ) +# }) test_that("parse_user_profiles() dispatches to opal method", { opal_con <- structure(list(), class = "opal") @@ -110,7 +110,7 @@ test_that("parse_user_profiles.opal() handles missing userInfo column", { test_that("parse_user_profiles.opal() parses JSON userInfo and NA correctly", { opal_con <- structure(list(), class = "opal") - json <- '{"firstName":"John","lastName":"Doe"}' + json <- '{"givenName":"John","familyName":"Doe"}' local_mocked_bindings( `opal.get` = function(...) { diff --git a/tests/testthat/test-utils-opal.R b/tests/testthat/test-utils-opal.R index 63899f5..dcc6835 100644 --- a/tests/testthat/test-utils-opal.R +++ b/tests/testthat/test-utils-opal.R @@ -154,10 +154,10 @@ test_that("update_project_datasets updates project entities of an RO-Crate", { test_that("user_perm_entity works for all values of 'permission'", { input_tbl <- tibble::tibble( - user = "dsuser", - user_id = "dsuser", - table = "tab1", - table_id = "tab1", + person = "dsuser", + person_id = "dsuser", + asset = "tab1", + asset_id = "tab1", permission = c( "view", "view-values", @@ -177,10 +177,10 @@ test_that("user_perm_entity works for all values of 'permission'", { test_that("user_perm_entity returns NULL for unknown 'permission'", { input_tbl <- tibble::tibble( - user = "dsuser", - user_id = "dsuser", - table = "tab1", - table_id = "tab1", + person = "dsuser", + person_id = "dsuser", + asset = "tab1", + asset_id = "tab1", permission = "INVALID" ) diff --git a/tests/testthat/test-utils-safe_data.R b/tests/testthat/test-utils-safe_data.R index 047408a..c9036db 100644 --- a/tests/testthat/test-utils-safe_data.R +++ b/tests/testthat/test-utils-safe_data.R @@ -68,7 +68,7 @@ test_that("extract_safe_data.rocrate copies Dataset entities", { src <- rocrateR::add_entity( src, entity = rocrateR::entity( - id = "#ds1", + id = "#asset:1", type = "Dataset", name = "Dataset 1" ) @@ -77,7 +77,7 @@ test_that("extract_safe_data.rocrate copies Dataset entities", { src <- rocrateR::add_entity( src, entity = rocrateR::entity( - id = "#ds2", + id = "#asset:2", type = "Dataset", name = "Dataset 2" ) @@ -102,7 +102,7 @@ test_that("extract_safe_data.rocrate filters Dataset entities by id", { src <- rocrateR::add_entity( src, entity = rocrateR::entity( - id = "#keep_me", + id = "#asset:keep_me", type = "Dataset", name = "Keep" ) @@ -111,7 +111,7 @@ test_that("extract_safe_data.rocrate filters Dataset entities by id", { src <- rocrateR::add_entity( src, entity = rocrateR::entity( - id = "#drop_me", + id = "#asset:drop_me", type = "Dataset", name = "Drop" ) @@ -119,13 +119,13 @@ test_that("extract_safe_data.rocrate filters Dataset entities by id", { # ignore warning of project not having tables associated suppressWarnings( - new_roc <- extract_safe_data(src, id = "#keep_me") + new_roc <- extract_safe_data(src, id = "#asset:keep_me") ) ents <- rocrateR::get_entity(new_roc, type = "Dataset") ids <- vapply(ents, function(e) e[["@id"]], character(1)) - expect_equal(ids, c("./", "#keep_me")) + expect_equal(ids, c("./", "#asset:keep_me")) }) test_that("extract_safe_data.rocrate errors when no Dataset entities exist", { @@ -151,7 +151,7 @@ test_that("flatten_safe_data.rocrate extracts id and name correctly", { roc <- rocrateR::add_entity( roc, entity = rocrateR::entity( - id = "#ds1", + id = "#asset:1", type = "Dataset", name = "Dataset One" ) @@ -160,7 +160,7 @@ test_that("flatten_safe_data.rocrate extracts id and name correctly", { roc <- rocrateR::add_entity( roc, entity = rocrateR::entity( - id = "#ds2", + id = "#asset:2", type = "Dataset", name = "Dataset Two" ) @@ -169,7 +169,7 @@ test_that("flatten_safe_data.rocrate extracts id and name correctly", { res <- flatten_safe_data(roc) expect_s3_class(res, "data.frame") - expect_true(all(c("id", "name") %in% names(res))) + expect_true(all(c("asset_id", "asset") %in% names(res))) expect_equal(nrow(res), 2) }) @@ -179,7 +179,7 @@ test_that("flatten_safe_data.rocrate filters by id argument", { roc <- rocrateR::add_entity( roc, entity = rocrateR::entity( - id = "#dsA", + id = "#asset:A", type = "Dataset", name = "A" ) @@ -188,16 +188,16 @@ test_that("flatten_safe_data.rocrate filters by id argument", { roc <- rocrateR::add_entity( roc, entity = rocrateR::entity( - id = "#dsB", + id = "#asset:B", type = "Dataset", name = "B" ) ) - res <- flatten_safe_data(roc, id = "#dsA") + res <- flatten_safe_data(roc, id = "#asset:A") expect_equal(nrow(res), 1) - expect_equal(res$id, "#dsA") + expect_equal(res$asset_id, "#asset:A") }) test_that("flatten_safe_data.rocrate returns empty tibble on error", { diff --git a/tests/testthat/test-utils-safe_people.R b/tests/testthat/test-utils-safe_people.R index 31bc897..9a304b2 100644 --- a/tests/testthat/test-utils-safe_people.R +++ b/tests/testthat/test-utils-safe_people.R @@ -61,7 +61,7 @@ test_that("extract_safe_people.opal iterates over all returned subject profiles" # terminate connection when done with tests withr::defer(opalr::opal.logout(opal_con)) - users_raw <- opalr::opal.get(opal_con, "/system/subject-profiles/") + users_raw <- opalr::oadmin.user_profiles(opal_con, df = FALSE) users_tbl <- dplyr::bind_rows(users_raw) expect_true(nrow(users_tbl) >= 1) @@ -200,9 +200,10 @@ test_that("flatten_safe_people.rocrate extracts person metadata correctly", { expect_s3_class(res, "data.frame") expect_true(all( - c("id", "name", "given_name", "family_name", "organisation") %in% names(res) + c("person_id", "name", "given_name", "family_name", "organisation") %in% + names(res) )) - expect_equal(res$id, "#p1") + expect_equal(res$person_id, "#p1") expect_equal(res$name, "Jane Doe") }) @@ -222,7 +223,7 @@ test_that("flatten_safe_people.rocrate filters by id argument", { res <- flatten_safe_people(roc, id = "#p2") expect_equal(nrow(res), 1) - expect_equal(res$id, "#p2") + expect_equal(res$person_id, "#p2") }) test_that("flatten_safe_people.rocrate returns empty tibble on malformed input", { diff --git a/tests/testthat/test-utils-safe_project.R b/tests/testthat/test-utils-safe_project.R index 7482061..a733d2c 100644 --- a/tests/testthat/test-utils-safe_project.R +++ b/tests/testthat/test-utils-safe_project.R @@ -215,8 +215,8 @@ test_that("flatten_safe_project.rocrate returns tibble", { # minimal stub for flatten_safe_data used internally flatten_safe_data <- function(x, ..., id = NULL) { tibble::tibble( - id = id, - name = c("table1", "table2") + asset_id = id, + asset = c("table1", "table2") ) } @@ -225,7 +225,9 @@ test_that("flatten_safe_project.rocrate returns tibble", { out <- flatten_safe_project(rocrate) expect_s3_class(out, "tbl_df") - expect_true(all(c("id", "project", "table") %in% names(out))) + expect_true(all( + c("project_id", "project", "asset_id", "asset") %in% names(out) + )) }) test_that("flatten_safe_project.rocrate extracts project metadata correctly", { @@ -258,8 +260,8 @@ test_that("flatten_safe_project.rocrate extracts project metadata correctly", { flatten_safe_data <- function(x, ..., id = NULL) { tibble::tibble( - id = id, - name = paste0("name_", id) + asset_id = id, + asset = paste0("name_", id) ) } @@ -268,7 +270,7 @@ test_that("flatten_safe_project.rocrate extracts project metadata correctly", { out <- flatten_safe_project(rocrate) expect_true("Project 1" %in% out$project) - expect_true(any(grepl("name_", out$table))) + expect_true(any(grepl("name_", out$asset))) }) test_that("flatten_safe_project.rocrate handles project without hasPart", { @@ -286,7 +288,7 @@ test_that("flatten_safe_project.rocrate handles project without hasPart", { out <- flatten_safe_project(rocrate) expect_equal(nrow(out), 1) - expect_true(is.na(out$table)) + expect_true(is.na(out$asset)) }) test_that("flatten_safe_project.rocrate returns empty tibble on error", { diff --git a/tests/testthat/test-utils-show.R b/tests/testthat/test-utils-show.R deleted file mode 100644 index 55c1799..0000000 --- a/tests/testthat/test-utils-show.R +++ /dev/null @@ -1,47 +0,0 @@ -test_that("show() prints expired credentials message", { - obj <- new( - "ArmadilloCredentials", - auth_type = "bearer", - token_type = "JWT", - expires_at = Sys.time() - 3600 - ) - - expect_output( - show(obj), - "Connection expired!" - ) -}) - -test_that("show() prints credential details when not expired", { - expires_at <- as.POSIXct("2030-01-01 00:00:00", tz = "UTC") - - obj <- new( - "ArmadilloCredentials", - auth_type = "bearer", - token_type = "JWT", - expires_at = expires_at - ) - - expect_output(show(obj), "") - expect_output(show(obj), "auth type: bearer") - expect_output(show(obj), "token type: JWT") - expect_output( - show(obj), - format(expires_at, tz = "UTC") - ) -}) - -test_that("show() output is correct for expired credentials", { - obj <- new( - "ArmadilloCredentials", - auth_type = "bearer", - token_type = "JWT", - expires_at = Sys.time() - 1 - ) - - out <- paste(capture.output(show(obj)), collapse = "\n") - - expect_true(grepl("", out)) - expect_true(grepl("Connection expired!", out)) - expect_true(grepl("armadillo_login", out)) -}) diff --git a/vignettes/deploy-local-datashield-server-with-opal.Rmd b/vignettes/deploy-local-datashield-server-with-opal.Rmd index 35bfac4..7326054 100644 --- a/vignettes/deploy-local-datashield-server-with-opal.Rmd +++ b/vignettes/deploy-local-datashield-server-with-opal.Rmd @@ -10,7 +10,8 @@ vignette: > ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, - comment = "#>" + comment = "#>", + cache = FALSE ) ``` diff --git a/vignettes/getting-started.Rmd b/vignettes/getting-started.Rmd new file mode 100644 index 0000000..d97ad2e --- /dev/null +++ b/vignettes/getting-started.Rmd @@ -0,0 +1,506 @@ +--- +title: "Getting Started" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Getting Started} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + cache = FALSE, + out.width = "100%", + cache = FALSE, + screenshot.opts = list(vwidth = 2000, vheight = 600, zoom = 3, selector = "div.html-widget") +) + +# save the built-in output hook +hook_output <- knitr::knit_hooks$get("output") +hook_message <- knitr::knit_hooks$get("message") + +# define a truncation helper +truncate_lines_tail <- function(x, lines) { + x <- unlist(strsplit(x, "\n")) + more <- "..." + if (length(lines) == 1) { + if (length(x) > lines) { + # truncate the output, but add `more` + # x <- c(head(x, n = lines), more) + x <- c(more, tail(x, n = lines)) + } + } else { + x <- c(more, x[lines], more) + } + paste(c(x, ""), collapse = "\n") +} + +# set the new hooks +knitr::knit_hooks$set( + output = function(x, options) { + lines <- options$out.lines + if (is.null(lines)) return(hook_output(x, options)) + x <- truncate_lines_tail(x, lines) + return(hook_output(x, options)) + }, + message = function(x, options) { + lines <- options$out.lines + if (is.null(lines)) return(hook_message(x, options)) + x <- truncate_lines_tail(x, lines) + return(hook_message(x, options)) + } +) + +# helper function to get the number of rows of an RO-Crate +rocrate_lines <- function(rocrate) { + rocrate_txt(rocrate) |> + length() +} + +# helper function to read RO-Crate as text file +rocrate_txt <- function(rocrate) { + # create tmp file to write a JSON file with the output + tmp_file <- tempfile(fileext = ".json") + on.exit(unlink(tmp_file, force = TRUE)) + # write RO-Crate in tmp file + rocrateR::write_rocrate(rocrate, tmp_file) + # read RO-Crate as text file + readLines(tmp_file) +} +``` + +```{r setup} +library(dsROCrate) +``` + +This tutorial assumes that you have an internet connection and can access +OBiBa's Opal demo server: https://opal-demo.obiba.org + +Alternatively, if you want to test a local deployment, please check out the +following vignette first: + +```{r, eval = FALSE} +vignette("deploy-local-datashield-server-with-opal", package = "dsROCrate") +``` + +--------------------- + +## 1. Creating your first RO-Crate + +### 1.1. Connect to an Opal server + +Here we will use OBiBa's Opal demo server: https://opal-demo.obiba.org/ which +can be accessed with the following login credentials: + +```{r} +# define global variables +## Opal server access +USERNAME <- "administrator" +USERPASS <- "password" +SERVER <- "https://opal-demo.obiba.org" +## Credentials for `dsuser` +### NOTE: this is only used to simulate an analysis and generate logs +DSUSERPASS <- "P@ssw0rd" +``` + +Next, define global variables used in generating the RO-Crate, such as project +name, asset (e.g., tables, resources, etc.) references (within the project) and +user identifiers. + +```{r} +## Five safes variables +PEOPLE <- "dsuser" +PROJECT <- "CNSIM" +TABLES <- c("CNSIM1") +``` + +#### Open connection + +Once the credentials and Five Safes variables are configured, we can start a +new session on the Opal server with the following command: + +```{r} +# login to local server with `USERNAME` and `USERPASS`. +o <- opalr::opal.login( + username = USERNAME, + password = USERPASS, + url = SERVER +) + +print(o) +``` + +### 1.2. Create a basic RO-Crate + +To create a basic RO-Crate, we will use the +[`{rocrateR}`](https://github.com/ResearchObject/ro-crate-r) package. This +package can be installed with the following command: + +```{r, eval = FALSE, echo = TRUE} +# install.packages("pak") +pak::pak("rocrateR") + +# for development version use +pak::pak("ResearchObject/ro-crate-r@dev") +``` + +Then, a basic RO-Crate can be created with the following command: + +```{r} +basic_rocrate <- rocrateR::rocrate_5s() +``` + +Note that this RO-Crate uses the [5s-crate](https://trefx.uk/5s-crate/0.4/) +profile. + +```{r} +print(basic_rocrate) +``` + +### 1.3. Add the _Five Safes_ Elements +#### Safe Data +To add details for Safe Data, use the function `dsROCrate::safe_data()`. + +```{r, out.lines=-(rocrate_lines(basic_rocrate) - 2)} +basic_rocrate <- o |> + dsROCrate::safe_data(rocrate = basic_rocrate, + project = PROJECT, + tables = TABLES) + +print(basic_rocrate) # note that the output will be truncated +``` + +#### Safe Project +To add details for Safe Project, use the function `dsROCrate::safe_project()`. + +```{r, out.lines=-(rocrate_lines(basic_rocrate) - 2)} +basic_rocrate <- o |> + dsROCrate::safe_project(rocrate = basic_rocrate, + project = PROJECT) + +print(basic_rocrate) # note that the output will be truncated +``` + +#### Safe People +To add details for Safe People, use the function `dsROCrate::safe_people()`. + +```{r safe_people, out.lines=-(rocrate_lines(basic_rocrate) + 1)} +basic_rocrate <- o |> + dsROCrate::safe_people(rocrate = basic_rocrate, user = PEOPLE) + +print(basic_rocrate) # note that the output will be truncated +``` + +#### Safe Setting +To add details for Safe Setting, use the function `dsROCrate::safe_setting()`. + +**⚠️NOTE:** The `dsROCrate::safe_setting` function requires administrator +privileges, so here, we will have to log in with administrator credentials (if +you used a non-administrator account previously). + +```{r, eval = FALSE} +# close previous connection +opalr::opal.logout(o) + +# open new connection as administrator +o <- opalr::opal.login( + username = "administrator", + password = "password", + url = SERVER +) +``` + +Then, we can proceed as per usual: +```{r, out.lines=-(rocrate_lines(basic_rocrate) - 2)} +basic_rocrate <- o |> + dsROCrate::safe_setting(rocrate = basic_rocrate) + +print(basic_rocrate) # note that the output will be truncated +``` + +#### Safe Outputs +To add details for Safe Outputs, use the function `dsROCrate::safe_output()`. +Currently, only log files from the operations executed by the user within a +specific period. Set the period using `logs_from` and `logs_to`. Additionally, +a list of functions executed by the user are extracted in a separate file/entity. + +**⚠️NOTE:** Similar to `dsROCrate::safe_setting`, the `dsROCrate::safe_output` +function requires of administrator rights, so here, we will have to log in with +administrator credentials: + +```{r, eval = FALSE} +# close previous connection +opalr::opal.logout(o) + +# open new connection as administrator +o <- opalr::opal.login( + username = "administrator", + password = "password", + url = SERVER +) +``` + +--------- + +##### DataSHIELD operations + +**⚠️NOTE:** Before extracting logs, ensure there is recent activity on the +server for testing purposes. This can be done using the following commands: + +###### Setup +You will need the following packages: + +```{r, eval = FALSE} +pak::pak("DSI") +pak::pak("DSOpal") +pak::pak("dsBaseClient") +``` + +###### Open connection +```{r, eval = TRUE} +# run some test commands with dsBaseClient +## needed to defined the OpalDriver class in the current environment +DSOpal::Opal() +## create new login object, note that here we use the `dsuser` +builder <- DSI::newDSLoginBuilder() +builder$append(server = "study1", + url = SERVER, + user = "dsuser", + password = DSUSERPASS, + driver = "OpalDriver") +logindata <- builder$build() +conns <- DSI::datashield.login(logins = logindata) +``` + +###### Simulate some operations +```{r, eval = TRUE} +## assign data +DSI::datashield.assign.table(conns["study1"], + symbol = "dsROCrate_test", + table = paste0(PROJECT, ".", TABLES[1]), + errors.print = TRUE) + +dsBaseClient::ds.ls(datasources = conns["study1"]) +dsBaseClient::ds.summary("dsROCrate_test") +``` + +```{r, echo = FALSE, warning = FALSE, message = FALSE} +# check if there are any logs available, if not simulate some operations +``` + +--------- + +Then, we can proceed as per usual: + +```{r safe_outputs_internal, echo=FALSE} +lines_before_safe_outputs <- rocrate_lines(basic_rocrate) +``` + +```{r safe_outputs} +basic_rocrate <- o |> + dsROCrate::safe_output(rocrate = basic_rocrate, + logs_from = Sys.time() - 60, # capture the last minute + logs_to = Sys.time()) +``` + +```{r, out.lines=-(lines_before_safe_outputs + 1)} +print(basic_rocrate) # note that the output will be truncated +``` + +### 1.4. Close connection + +```{r} +opalr::opal.logout(o) +``` + +### 1.5. Bag/Save RO-Crate + +The resulting RO-Crate can be stored into an RO-Crate bag/archive with the +function `rocrateR::bag_rocrate`: + +```{r} +# create temp directory +tmp_path_bag <- file.path(tempdir(), "dsROCrate-getting-started") +dir.create(tmp_path_bag, showWarnings = FALSE) + +# create RO-Crate bag +path_to_rocrate_bag <- basic_rocrate |> + rocrateR::bag_rocrate(path = tmp_path_bag, overwrite = TRUE) +``` + +We can explore the contents with the following commands: + +```{r} +# extract files in temporary directory +path_to_rocrate_bag |> + # extract contents inside /tmp_path_bag/ROC + rocrateR::unbag_rocrate(output = file.path(tmp_path_bag, "ROC"), quiet = TRUE) |> + # create tree with the files + fs::dir_tree() +``` + +### 1.6. Clean working directory + +```{r} +unlink(tmp_path_bag, recursive = TRUE, force = TRUE) +``` + + +
+ +## 2. Auditing RO-Crates and servers + +### 2.1. Audit People + +##### List accessible tables within a project for an user +```{r, warning=FALSE} +safe_people_crate_v1 <- opalr::opal.login( + username = USERNAME, + password = USERPASS, + url = SERVER +) |> + dsROCrate::audit(user = "dsuser", project = "CNSIM") + +print(safe_people_crate_v1) +``` + +###### Markdown report + +A markdown report can be created with an overview and details for an RO-Crate, +using the `dsROCrate::report`: + +**Only generate .Rmd file** + +```{r safe_people_crate_audit_v1} +safe_people_crate_v1_rmd <- tempfile(fileext = ".Rmd") # temporary file + +safe_people_crate_contents <- safe_people_crate_v1 |> + dsROCrate::report(filepath = safe_people_crate_v1_rmd, render = FALSE) + +# display Overview diagram +safe_people_crate_contents$overview_diagram + +# display Overview data (Safe People, Safe Projects and Safe Data) +safe_people_crate_contents$overview_data |> + knitr::kable() +``` + +**Render and display report (HTML)** + +```{r, eval = FALSE} +safe_people_crate_v1 |> + dsROCrate::report(filepath = safe_people_crate_v1_rmd, + title = "DataSHIELD Safe People - Audit Report", + render = TRUE, + overwrite = TRUE) +``` + +### 2.2. Audit Project + +##### List users and dataset/table level permissions within a project +```{r, warning=FALSE} +safe_project_crate_v1 <- opalr::opal.login( + username = USERNAME, + password = USERPASS, + url = SERVER +) |> + dsROCrate::audit(project = "CNSIM") + +print(safe_project_crate_v1) +``` + +###### Markdown report + +A markdown report can be created with an overview and details for an RO-Crate, +using the `dsROCrate::report`: + +**Only generate .Rmd file** + +```{r safe_project_crate_audit_v1} +safe_project_crate_v1_rmd <- tempfile(fileext = ".Rmd") # temporary file + +safe_project_crate_contents <- safe_project_crate_v1 |> + dsROCrate::report(filepath = safe_project_crate_v1_rmd, render = FALSE) + +# display Overview diagram +safe_project_crate_contents$overview_diagram + +# display Overview data (Safe People, Safe Projects and Safe Data) +safe_project_crate_contents$overview_data |> + knitr::kable() +``` + +**Render and display report (HTML)** + +```{r, eval = FALSE} +safe_project_crate_v1 |> + dsROCrate::report(filepath = safe_project_crate_v1_rmd, + title = "DataSHIELD Safe Project - Audit Report", + render = TRUE, + overwrite = TRUE) +``` + +
+ +### 2.3. Audit Study + +##### List users and dataset/table level permissions within a study (i.e., multiple servers) +```{r, warning=FALSE} +study_crate_v1 <- + list( + "opal_test" = opalr::opal.login( + username = USERNAME, + password = USERPASS, + url = "https://opal-test.obiba.org" + ), + "opal_demo" = opalr::opal.login( + username = USERNAME, + password = USERPASS, + url = "https://opal-demo.obiba.org" + ) + ) |> + dsROCrate::audit(project = "CNSIM") + +print(study_crate_v1) +``` + +###### Markdown report + +A markdown report can be created with an overview and details for an RO-Crate, +using the `dsROCrate::report`: + +**Only generate .Rmd file** + +```{r study_crate_audit_v1} +study_crate_v1_rmd <- tempfile(fileext = ".Rmd") # temporary file + +safe_project_crate_contents <- study_crate_v1 |> + dsROCrate::report(filepath = study_crate_v1_rmd, render = FALSE) + +# display Overview diagram +safe_project_crate_contents$overview_diagram + +# display Overview data (Safe People, Safe Projects and Safe Data) +safe_project_crate_contents$overview_data |> + knitr::kable() +``` + +**Render and display report (HTML)** + +```{r, eval = FALSE} +study_crate_v1 |> + dsROCrate::report(filepath = study_crate_v1_rmd, + title = "DataSHIELD Study audit", + render = TRUE, + overwrite = TRUE) +``` + +
+ +```{r, echo = FALSE, message = FALSE, error = FALSE} +unlink(safe_people_crate_v1_rmd, TRUE, TRUE) +unlink(safe_project_crate_v1_rmd, TRUE, TRUE) +unlink(study_crate_v1_rmd, TRUE, TRUE) +```