From 3346faec3d1c0a6d180a1b60cab4176c2b27fa95 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Mon, 19 Sep 2022 16:17:18 +0200 Subject: [PATCH] Add helper functions for OAUTH2 authentication (#109) (#111) * Add helper functions for OAUTH2 authentication (#109) * Add token to d2Session object * Add OAUTH login function * Update NAMESPACE * Documentation and cleanup * Update rocker verse R version * Fix R6 import * Add lintr to suggests * Update DESCRIPTION * Bump version to 0.6.0 --- .circleci/config.yml | 2 +- DESCRIPTION | 14 ++++--- NAMESPACE | 2 + NEWS.md | 8 +++- R/loginToDATIM.R | 90 +++++++++++++++++++++++++++++++++++++++- man/d2Session.Rd | 10 ++++- man/loginToDATIMOAuth.Rd | 56 +++++++++++++++++++++++++ renv.lock | 56 ++++++++----------------- 8 files changed, 189 insertions(+), 49 deletions(-) create mode 100644 man/loginToDATIMOAuth.Rd diff --git a/.circleci/config.yml b/.circleci/config.yml index ea0566da..b72dd2a9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ description: Datapackr Test Suite jobs: build: docker: - - image: rocker/verse:3.6.3 + - image: rocker/verse:4.1.1 steps: - checkout - restore_cache: diff --git a/DESCRIPTION b/DESCRIPTION index 82de55b9..60055a67 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Package: datimutils Type: Package Title: Utilities for interacting with the DATIM api from R -Version: 0.5.3 -Date: 2022-09-08 +Version: 0.6.0 +Date: 2022-09-19 Authors@R: c( person("Scott", "Jackson", email = "sjackson@baosystems.com", @@ -21,14 +21,15 @@ Authors@R: Description: General utilities for interacting with the DATIM api from R. URL: https://github.com/pepfar-datim/datimutils BugReports: https://github.com/pepfar-datim/datimutils -Depends: R (>= 3.6.0) +Depends: R (>= 4.1.1) Imports: tidyr (>= 0.8.3), jsonlite (>= 1.6), stringi (>= 1.7.6), httr (>= 1.4.0), keyring, - rlang + rlang, + R6 (>= 2.5.1) Suggests: devtools (>= 2.0.1), dplyr (>= 0.8.3), @@ -38,9 +39,10 @@ Suggests: knitr, rmarkdown, assertthat, - testthat (>= 2.1.0) + testthat (>= 2.1.0), + lintr(>= 3.0.0) License: GPL-3 + file LICENSE Encoding: UTF-8 LazyData: true -RoxygenNote: 7.2.0 +RoxygenNote: 7.2.1 VignetteBuilder: knitr diff --git a/NAMESPACE b/NAMESPACE index 549ac2df..eef31bb2 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -58,6 +58,8 @@ export(getUserGroups) export(listMechs) export(listSqlViews) export(loginToDATIM) +export(loginToDATIMOAuth) export(make_dim) export(make_fil) export(metadataFilter) +import(R6) diff --git a/NEWS.md b/NEWS.md index daf36bd4..5e7f6420 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,8 +1,14 @@ -# datimutils 0.5.3 +# datimutils 0.6.0 + +## New features +* Adds `loginToDATIMOAuth` function to assist with OAUTH2.0 authentication. + +# datimutils 0.5.4 ## Bug fixes * Fixes error conditions in getDataValueSets. + # datimutils 0.5.2 ## Bug fixes diff --git a/R/loginToDATIM.R b/R/loginToDATIM.R index f9b7bfac..ec672f21 100644 --- a/R/loginToDATIM.R +++ b/R/loginToDATIM.R @@ -1,3 +1,5 @@ +#' @import R6 +#' d2Session <- R6::R6Class("d2Session", #' @title d2Session public = list( @@ -17,7 +19,10 @@ d2Session <- R6::R6Class("d2Session", handle = NULL, #' @field me dhis2 api/me response me = NULL, + #' @field max_cache_age Maximum time responses should be cached max_cache_age = NULL, + #' @field token An httr OAUTH2 token + token = NULL, #' @description #' Create a new DHISLogin object #' @param config_path Configuration file path @@ -25,16 +30,19 @@ d2Session <- R6::R6Class("d2Session", #' @param handle httr handle to be used for dhis2 #' connections #' @param me DHIS2 me response object + #' @param token OAUTH2 token initialize = function(config_path = NA_character_, base_url, handle, - me) { + me, + token) { self$config_path <- config_path self$me <- me self$user_orgunit <- me$organisationUnits$id self$base_url <- base_url self$username <- me$userCredentials$username self$handle <- handle + self$token <- token } ) ) @@ -203,7 +211,8 @@ loginToDATIM <- function(config_path = NULL, d2Session$new(config_path = config_path, base_url = base_url, handle = handle, - me = me), + me = me, + token = NULL), envir = d2_session_envir) } else if (r$status == 302L) { stop("Unable to authenticate due to DATIM currently undergoing maintenance. @@ -221,3 +230,80 @@ loginToDATIM <- function(config_path = NULL, stop("An unknowon error has occured during authentication!") } } + + + +#' @title datimutils::loginToDATIMOAuth(base_url = Sys.getenv("BASE_URL"), +#' token = token, +#' app = oauth_app, +#' api = oauth_api, +#' redirect_uri= APP_URL, +#' scope = oauth_scope, +#' d2_session_envir = parent.env(environment())) +#' +#' @param base_url URL of the DHIS2 server +#' @param token An OAUTH2.0 token object. Will be created if not supplied. +#' @param redirect_uri The redirect URI which should be used after +#' successful authentication with the server. +#' @param app An httr OAUTH app object. +#' @param api An hjttr OAUTH endpoint. +#' @param scope A character vector of scopes which should be requested. +#' @param d2_session_name the variable name for the d2Session object. +#' The default name is d2_default_session and will be used by other datimutils +#' functions by default when connecting to datim. Generally a custom name +#' should only be needed if you need to log into two seperate DHIS2 instances +#' at the same time. If you create a d2Session object with a custom name then +#' this object must be passed to other datimutils functions explicitly +#' @param d2_session_envir the environment in which to place the R6 login +#' object, default is the immediate calling environment +#' +#' @export +#' + +loginToDATIMOAuth <- function( + base_url = NULL, + token = NULL, + redirect_uri = NULL, + app = NULL, + api = NULL, + scope = NULL, + d2_session_name = "d2_default_session", + d2_session_envir = parent.frame()) { + + if (is.null(token)) { + token <- httr::oauth2.0_token( + app = app, + endpoint = api, + scope = scope, + use_basic_auth = TRUE, + oob_value = redirect_uri, + cache = FALSE + ) + } else { + token <- token #For Shiny + } + + # form url + url <- utils::URLencode(URL = paste0(base_url, "api", "/me")) + handle <- httr::handle(base_url) + #Get Request + r <- httr::GET( + url, + httr::config(token = token), + httr::timeout(60), + handle = handle + ) + + if (r$status_code != 200L) { + stop("Could not authenticate you with the server!") + } else { + me <- jsonlite::fromJSON(httr::content(r, as = "text")) + # create the session object in the calling environment of the login function + assign(d2_session_name, + d2Session$new(base_url = base_url, + handle = handle, + me = me, + token = token), + envir = d2_session_envir) + } +} diff --git a/man/d2Session.Rd b/man/d2Session.Rd index 347e91a3..4b50abe1 100644 --- a/man/d2Session.Rd +++ b/man/d2Session.Rd @@ -26,6 +26,10 @@ organisation unit} with the DHIS2 instance.} \item{\code{me}}{dhis2 api/me response} + +\item{\code{max_cache_age}}{Maximum time responses should be cached} + +\item{\code{token}}{An httr OAUTH2 token} } \if{html}{\out{}} } @@ -42,7 +46,7 @@ with the DHIS2 instance.} \subsection{Method \code{new()}}{ Create a new DHISLogin object \subsection{Usage}{ -\if{html}{\out{
}}\preformatted{d2Session$new(config_path = NA_character_, base_url, handle, me)}\if{html}{\out{
}} +\if{html}{\out{
}}\preformatted{d2Session$new(config_path = NA_character_, base_url, handle, me, token)}\if{html}{\out{
}} } \subsection{Arguments}{ @@ -56,6 +60,10 @@ Create a new DHISLogin object connections} \item{\code{me}}{DHIS2 me response object} + + +\item{\code{token}}{OAUTH2 token} + } \if{html}{\out{}} } diff --git a/man/loginToDATIMOAuth.Rd b/man/loginToDATIMOAuth.Rd new file mode 100644 index 00000000..6e72da09 --- /dev/null +++ b/man/loginToDATIMOAuth.Rd @@ -0,0 +1,56 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/loginToDATIM.R +\name{loginToDATIMOAuth} +\alias{loginToDATIMOAuth} +\title{datimutils::loginToDATIMOAuth(base_url = Sys.getenv("BASE_URL"), +token = token, +app = oauth_app, +api = oauth_api, +redirect_uri= APP_URL, +scope = oauth_scope, +d2_session_envir = parent.env(environment()))} +\usage{ +loginToDATIMOAuth( + base_url = NULL, + token = NULL, + redirect_uri = NULL, + app = NULL, + api = NULL, + scope = NULL, + d2_session_name = "d2_default_session", + d2_session_envir = parent.frame() +) +} +\arguments{ +\item{base_url}{URL of the DHIS2 server} + +\item{token}{An OAUTH2.0 token object. Will be created if not supplied.} + +\item{redirect_uri}{The redirect URI which should be used after +successful authentication with the server.} + +\item{app}{An httr OAUTH app object.} + +\item{api}{An hjttr OAUTH endpoint.} + +\item{scope}{A character vector of scopes which should be requested.} + +\item{d2_session_name}{the variable name for the d2Session object. +The default name is d2_default_session and will be used by other datimutils +functions by default when connecting to datim. Generally a custom name +should only be needed if you need to log into two seperate DHIS2 instances +at the same time. If you create a d2Session object with a custom name then +this object must be passed to other datimutils functions explicitly} + +\item{d2_session_envir}{the environment in which to place the R6 login +object, default is the immediate calling environment} +} +\description{ +datimutils::loginToDATIMOAuth(base_url = Sys.getenv("BASE_URL"), +token = token, +app = oauth_app, +api = oauth_api, +redirect_uri= APP_URL, +scope = oauth_scope, +d2_session_envir = parent.env(environment())) +} diff --git a/renv.lock b/renv.lock index a3e75fdd..ae10ec8f 100644 --- a/renv.lock +++ b/renv.lock @@ -1,6 +1,6 @@ { "R": { - "Version": "3.6.3", + "Version": "4.2.1", "Repositories": [ { "Name": "CRAN", @@ -13,18 +13,10 @@ "Package": "R6", "Version": "2.5.1", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "470851b6d5d0ac559e9d01bb352b4021", "Requirements": [] }, - "Rcpp": { - "Package": "Rcpp", - "Version": "1.0.7", - "Source": "Repository", - "Repository": "RSPM", - "Hash": "dab19adae4440ae55aa8a9d238b246bb", - "Requirements": [] - }, "askpass": { "Package": "askpass", "Version": "1.1", @@ -45,10 +37,10 @@ }, "backports": { "Package": "backports", - "Version": "1.2.1", + "Version": "1.4.1", "Source": "Repository", "Repository": "CRAN", - "Hash": "644043219fc24e190c2f620c1a380a69", + "Hash": "c39fbec8a30d23e721980b8afb31984c", "Requirements": [] }, "base64enc": { @@ -111,7 +103,7 @@ "Package": "cachem", "Version": "1.0.6", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "648c5b3d71e6a37e3043617489a0a0e9", "Requirements": [ "fastmap", @@ -231,7 +223,7 @@ "Package": "devtools", "Version": "2.4.3", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "fc35e13bb582e5fe6f63f3d647a4cbe5", "Requirements": [ "callr", @@ -323,7 +315,7 @@ "Package": "fastmap", "Version": "1.1.0", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "77bd60a6157420d4ffa93b27cf6a58b8", "Requirements": [] }, @@ -331,7 +323,7 @@ "Package": "filelock", "Version": "1.0.2", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "38ec653c2613bed60052ba3787bd8a2c", "Requirements": [] }, @@ -384,7 +376,7 @@ "Package": "gitcreds", "Version": "0.1.1", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "f3aefccc1cc50de6338146b62f115de8", "Requirements": [] }, @@ -424,7 +416,7 @@ "Package": "htmltools", "Version": "0.5.2", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "526c484233f42522278ab06fb185cb26", "Requirements": [ "base64enc", @@ -465,7 +457,7 @@ "Package": "ini", "Version": "0.3.1", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "6154ec2223172bce8162d4153cda21f7", "Requirements": [] }, @@ -666,7 +658,7 @@ "Package": "prettyunits", "Version": "1.1.1", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "95ef9167b75dde9d2ccc3c7528393e7e", "Requirements": [] }, @@ -713,18 +705,6 @@ "rlang" ] }, - "qpdf": { - "Package": "qpdf", - "Version": "1.1", - "Source": "Repository", - "Repository": "CRAN", - "Hash": "5a2907cd44279b5861ac5f813465b5c9", - "Requirements": [ - "Rcpp", - "askpass", - "curl" - ] - }, "rappdirs": { "Package": "rappdirs", "Version": "0.3.3", @@ -839,10 +819,10 @@ }, "roxygen2": { "Package": "roxygen2", - "Version": "7.2.0", + "Version": "7.2.1", "Source": "Repository", "Repository": "CRAN", - "Hash": "b390c1d54fcd977cda48588e6172daba", + "Hash": "da1f278262e563c835345872f2fef537", "Requirements": [ "R6", "brew", @@ -873,7 +853,7 @@ "Package": "rstudioapi", "Version": "0.13", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "06c85365a03fdaf699966cc1d3cf53ea", "Requirements": [] }, @@ -944,7 +924,7 @@ "Package": "sys", "Version": "3.4", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "b227d13e29222b4574486cfcbde077fa", "Requirements": [] }, @@ -1138,7 +1118,7 @@ "Package": "whisker", "Version": "0.4", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "ca970b96d894e90397ed20637a0c1bbe", "Requirements": [] }, @@ -1196,7 +1176,7 @@ "Package": "zip", "Version": "2.2.0", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "c7eef2996ac270a18c2715c997a727c5", "Requirements": [] }