From 286d595e311d0fed1417f0d15eb005546f32d1bc Mon Sep 17 00:00:00 2001 From: Hong Ooi Date: Fri, 15 Oct 2021 18:15:02 +1100 Subject: [PATCH 1/4] copy logic from AzureGraph --- R/az_login.R | 126 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 95 insertions(+), 31 deletions(-) diff --git a/R/az_login.R b/R/az_login.R index be632d2..80928f8 100644 --- a/R/az_login.R +++ b/R/az_login.R @@ -146,43 +146,22 @@ get_azure_login <- function(tenant="common", selection=NULL, refresh=TRUE) stop(msg, call.=FALSE) } - if(length(this_login) == 1 && is.null(selection)) - selection <- 1 - else if(is.null(selection)) - { - tokens <- lapply(this_login, function(f) - readRDS(file.path(AzureR_dir(), f))) + message("Loading Microsoft Graph login for ", format_tenant(tenant)) - choices <- sapply(tokens, function(token) - { - app <- token$client$client_id - paste0("App ID: ", app, "\n Authentication method: ", token$auth_type) - }) + # do we need to choose which login client to use? + have_selection <- !is.null(selection) + have_auth_spec <- any(!is.null(app), !is.null(scopes), !is.null(auth_type)) - msg <- paste0("Choose an Azure Resource Manager login for ", format_tenant(tenant)) - selection <- utils::menu(choices, title=msg) - } + token <- if(length(this_login) > 1 || have_selection || have_auth_spec) + choose_token(this_login, selection, app, scopes, auth_type) + else load_azure_token(this_login) - if(selection == 0) + if(is.null(token)) return(NULL) - file <- if(is.numeric(selection)) - this_login[selection] - else if(is.character(selection)) - this_login[which(this_login == selection)] # force an error if supplied hash doesn't match available logins - - file <- file.path(AzureR_dir(), file) - if(is_empty(file) || !file.exists(file)) - stop("Azure Active Directory token not found for this login", call.=FALSE) - - message("Loading Azure Resource Manager login for ", format_tenant(tenant)) - - token <- readRDS(file) - client <- az_rm$new(token=token) - + client <- ms_graph$new(token=token) if(refresh) client$token$refresh() - client } @@ -258,4 +237,89 @@ format_tenant <- function(tenant) if(tenant == "common") "default tenant" else paste0("tenant '", tenant, "'") -} \ No newline at end of file +} + + +# algorithm for choosing a token: +# if given a hash, choose it (error if no match) +# otherwise if given a number, use it (error if out of bounds) +# otherwise if given any of app|scopes|auth_type, use those (error if no match, ask if multiple matches) +# otherwise ask +choose_token <- function(hashes, selection, app, scopes, auth_type) +{ + if(is.character(selection)) + { + if(!(selection %in% hashes)) + stop("Token with selected hash not found", call.=FALSE) + return(load_azure_token(selection)) + } + + if(is.numeric(selection)) + { + if(selection <= 0 || selection > length(hashes)) + stop("Invalid numeric selection", call.=FALSE) + return(load_azure_token(hashes[selection])) + } + + tokens <- lapply(hashes, load_azure_token) + ok <- rep(TRUE, length(tokens)) + + # filter down list of tokens based on auth criteria + if(!is.null(app) || !is.null(scopes) || !is.null(auth_type)) + { + if(!is.null(scopes)) + scopes <- tolower(scopes) + + # look for matching token + for(i in seq_along(hashes)) + { + app_match <- scope_match <- auth_match <- TRUE + + if(!is.null(app) && tokens[[i]]$client$client_id != app) + app_match <- FALSE + if(!is.null(scopes)) + { + # AAD v1.0 tokens do not have scopes + if(is.null(tokens[[i]]$scope)) + scope_match <- is.na(scopes) + else + { + tok_scopes <- tolower(basename(grep("^.+://", tokens[[i]]$scope, value=TRUE))) + if(!setequal(scopes, tok_scopes)) + scope_match <- FALSE + } + } + if(!is.null(auth_type) && tokens[[i]]$auth_type != auth_type) + auth_match <- FALSE + + if(!app_match || !scope_match || !auth_match) + ok[i] <- FALSE + } + } + + tokens <- tokens[ok] + if(length(tokens) == 0) + stop("No tokens found with selected authentication parameters", call.=FALSE) + else if(length(tokens) == 1) + return(tokens[[1]]) + + # bring up a menu + tenant <- tokens[[1]]$tenant + choices <- sapply(tokens, function(token) + { + app <- token$client$client_id + scopes <- if(!is.null(token$scope)) + paste(tolower(basename(grep("^.+://", token$scope, value=TRUE))), collapse=" ") + else "" + paste0("App ID: ", app, + "\n Scopes: ", scopes, + "\n Authentication method: ", token$auth_type, + "\n MD5 Hash: ", token$hash()) + }) + msg <- paste0("Choose a Microsoft Graph login for ", format_tenant(tenant)) + selection <- utils::menu(choices, title=msg) + if(selection == 0) + invisible(NULL) + else tokens[[selection]] +} + From 47e4bfcf5e1debd0b57a10e52d0b15c8bfae4565 Mon Sep 17 00:00:00 2001 From: Hong Ooi Date: Mon, 18 Oct 2021 16:38:55 +1100 Subject: [PATCH 2/4] get it working --- R/az_login.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/R/az_login.R b/R/az_login.R index 80928f8..58d3a60 100644 --- a/R/az_login.R +++ b/R/az_login.R @@ -130,7 +130,7 @@ create_azure_login <- function(tenant="common", app=.az_cli_app_id, #' @rdname azure_login #' @export -get_azure_login <- function(tenant="common", selection=NULL, refresh=TRUE) +get_azure_login <- function(tenant="common", selection=NULL, app=NULL, scopes=NULL, auth_type=NULL, refresh=TRUE) { if(!dir.exists(AzureR_dir())) stop("AzureR data directory does not exist; cannot load saved logins") @@ -146,7 +146,7 @@ get_azure_login <- function(tenant="common", selection=NULL, refresh=TRUE) stop(msg, call.=FALSE) } - message("Loading Microsoft Graph login for ", format_tenant(tenant)) + message("Loading Azure Resource Manager login for ", format_tenant(tenant)) # do we need to choose which login client to use? have_selection <- !is.null(selection) @@ -159,7 +159,7 @@ get_azure_login <- function(tenant="common", selection=NULL, refresh=TRUE) if(is.null(token)) return(NULL) - client <- ms_graph$new(token=token) + client <- az_rm$new(token=token) if(refresh) client$token$refresh() client From 2dc4c960025a212d79f9e7ba6eb6b2399e58e102 Mon Sep 17 00:00:00 2001 From: Hong Ooi Date: Mon, 18 Oct 2021 16:43:35 +1100 Subject: [PATCH 3/4] update doc --- DESCRIPTION | 2 +- NEWS.md | 4 ++++ man/az_subscription.Rd | 2 +- man/azure_login.Rd | 3 ++- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index e973ed4..6cd6639 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: AzureRMR Title: Interface to 'Azure Resource Manager' -Version: 2.4.2 +Version: 2.4.2.9000 Authors@R: c( person("Hong", "Ooi", , "hongooi73@gmail.com", role = c("aut", "cre")), person("Microsoft", role="cph") diff --git a/NEWS.md b/NEWS.md index 8499512..566ca01 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ +# AzureRMR 2.4.2.9000 + +- Some tweaks to the internal logic for retrieving ARM logins. + # AzureRMR 2.4.2 - Replace the old "Service principal" vignette with an "Authentication basics" vignette, which provides more information on common authentication flows. diff --git a/man/az_subscription.Rd b/man/az_subscription.Rd index c226c30..887d4c1 100644 --- a/man/az_subscription.Rd +++ b/man/az_subscription.Rd @@ -20,7 +20,7 @@ Class representing an Azure subscription. \item \code{delete_resource_group(name, confirm=TRUE)}: Delete a resource group, after asking for confirmation. \item \code{resource_group_exists(name)}: Check if a resource group exists. \item \code{list_resources(filter, expand, top)}: List all resources deployed under this subscription. \code{filter}, \code{expand} and \code{top} are optional arguments to filter the results; see the \href{https://docs.microsoft.com/en-us/rest/api/resources/resources/list}{Azure documentation} for more details. If \code{top} is specified, the returned list will have a maximum of this many items. -\item \code{list_locations(info=c("partial", "all"))}: List locations available. The default \code{info="partial"} returns a subset of all the information about each location; set \code{info="all"} to return everything. +\item \code{list_locations(info=c("partial", "all"))}: List locations available. The default \code{info="partial"} returns a subset of the information about each location; set \code{info="all"} to return everything. \item \code{get_provider_api_version(provider, type, which=1, stable_only=TRUE)}: Get the current API version for the given resource provider and type. If no resource type is supplied, returns a vector of API versions, one for each resource type for the given provider. If neither provider nor type is supplied, returns the API versions for all resources and providers. Set \code{stable_only=FALSE} to allow preview APIs to be returned. Set \code{which} to a number > 1 to return an API other than the most recent. \item \code{do_operation(...)}: Carry out an operation. See 'Operations' for more details. \item \code{create_lock(name, level)}: Create a management lock on this subscription (which will propagate to all resources within it). diff --git a/man/azure_login.Rd b/man/azure_login.Rd index 8024823..a4ebd4b 100644 --- a/man/azure_login.Rd +++ b/man/azure_login.Rd @@ -14,7 +14,8 @@ create_azure_login(tenant = "common", app = .az_cli_app_id, config_file = NULL, token = NULL, graph_host = "https://graph.microsoft.com/", ...) -get_azure_login(tenant = "common", selection = NULL, refresh = TRUE) +get_azure_login(tenant = "common", selection = NULL, app = NULL, + scopes = NULL, auth_type = NULL, refresh = TRUE) delete_azure_login(tenant = "common", confirm = TRUE) From 4f0c9b4f2821d4596716f734674d9b2a18720037 Mon Sep 17 00:00:00 2001 From: Hong Ooi Date: Mon, 18 Oct 2021 16:58:21 +1100 Subject: [PATCH 4/4] clarify --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 566ca01..4f0ca30 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,6 @@ # AzureRMR 2.4.2.9000 -- Some tweaks to the internal logic for retrieving ARM logins. +- Some tweaks to the logic for retrieving ARM login objects. Like with `AzureGraph::get_graph_login`, `get_azure_login` now has `app`, `scopes` and `auth_type` arguments to let you specify a particular login to retrieve. # AzureRMR 2.4.2