зеркало из https://github.com/Azure/AzureRMR.git
Merge pull request #10 from cloudyr/import_auth
This commit is contained in:
Коммит
52dc74ba75
|
@ -15,15 +15,16 @@ VignetteBuilder: knitr
|
|||
Depends:
|
||||
R (>= 3.3)
|
||||
Imports:
|
||||
AzureAuth,
|
||||
utils,
|
||||
httr (>= 1.3),
|
||||
openssl,
|
||||
jsonlite,
|
||||
R6,
|
||||
rappdirs
|
||||
R6
|
||||
Suggests:
|
||||
knitr,
|
||||
testthat,
|
||||
httpuv
|
||||
Roxygen: list(markdown=TRUE)
|
||||
RoxygenNote: 6.1.0.9000
|
||||
Remotes: cloudyr/AzureAuth
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# Generated by roxygen2: do not edit by hand
|
||||
|
||||
export(AzureRMR_dir)
|
||||
export(AzureToken)
|
||||
export(AzureR_dir)
|
||||
export(az_resource)
|
||||
export(az_resource_group)
|
||||
export(az_rm)
|
||||
|
@ -30,6 +29,5 @@ export(is_url)
|
|||
export(list_azure_logins)
|
||||
export(list_azure_tokens)
|
||||
export(named_list)
|
||||
export(normalize_guid)
|
||||
export(normalize_tenant)
|
||||
import(AzureAuth)
|
||||
importFrom(utils,modifyList)
|
||||
|
|
8
NEWS.md
8
NEWS.md
|
@ -4,14 +4,8 @@
|
|||
|
||||
* Allow authentication without having to create a service principal first, by leveraging the Azure CLI cross-platform app. It's still recommended to create your own SP for authentication, if possible.
|
||||
* New `create_azure_login`, `get_azure_login` and `delete_azure_login` functions to handle ARM authentication. While directly calling `az_rm$new()` will still work, it's recommended to use `create_azure_login` and `get_azure_login` going forward. Login credentials will be saved and reused for subsequent sessions (see below).
|
||||
* `get_azure_token` significantly revamped. It now supports four authentication methods for obtaining AAD tokens:
|
||||
- Client credentials (what you would use with a "web app" registered service principal)
|
||||
- Authorization code (for a "native" service principal)
|
||||
- Device code
|
||||
- With a username and password (resource owner grant)
|
||||
* `get_azure_token` will now cache AAD tokens and refresh them for subsequent sessions. Tokens are cached in a user-specific configuration directory, using the rappdirs package (unlike httr, which saves them in a special file in the R working directory).
|
||||
* By default, use the latest _stable_ API version when interacting with resources. `az_resource$set_api_version` gains a new argument `stable_only` which defaults to `TRUE`; set this to `FALSE` if you want the latest preview version.
|
||||
* Token acquisition logic will shortly move to a new package, to allow it to be used by other packages independently of the Resource Manager interface.
|
||||
* Token acquisition logic substantially enhanced and moved to a new package, [AzureAuth](https://github.com/cloudyr/AzureAuth). `get_azure_token` now supports four authentication methods for obtaining tokens (`client_credentials`, `authorization_code`, `device_code` and `resource_owner`). Tokens are also automatically cached and retrieved for use in subsequent sessions, without needing the user to reauthenticate. See the AzureAuth documentation for more details.
|
||||
|
||||
## Other changes
|
||||
|
||||
|
|
38
R/AzureRMR.R
38
R/AzureRMR.R
|
@ -1,12 +1,14 @@
|
|||
#' @import AzureAuth
|
||||
#' @importFrom utils modifyList
|
||||
NULL
|
||||
|
||||
|
||||
.onLoad <- function(libname, pkgname)
|
||||
{
|
||||
azure_api_version="2018-05-01"
|
||||
options(azure_api_version=azure_api_version)
|
||||
|
||||
make_AzureRMR_dir()
|
||||
make_AzureR_dir()
|
||||
|
||||
invisible(NULL)
|
||||
}
|
||||
|
@ -17,40 +19,18 @@ NULL
|
|||
|
||||
|
||||
# create a directory for saving creds -- ask first, to satisfy CRAN requirements
|
||||
make_AzureRMR_dir <- function()
|
||||
make_AzureR_dir <- function()
|
||||
{
|
||||
AzureRMR_dir <- AzureRMR_dir()
|
||||
if(!dir.exists(AzureRMR_dir) && interactive())
|
||||
AzureR_dir <- AzureR_dir()
|
||||
if(!dir.exists(AzureR_dir) && interactive())
|
||||
{
|
||||
yn <- readline(paste0(
|
||||
"AzureRMR can cache Azure Active Directory tokens and Resource Manager logins in the directory:\n\n",
|
||||
AzureRMR_dir, "\n\n",
|
||||
"AzureRMR can cache Azure Resource Manager logins in the directory:\n\n",
|
||||
AzureR_dir, "\n\n",
|
||||
"This saves you having to re-authenticate with Azure in future sessions. Create this directory? (Y/n) "))
|
||||
if(tolower(substr(yn, 1, 1)) == "n")
|
||||
return(invisible(NULL))
|
||||
|
||||
dir.create(AzureRMR_dir, recursive=TRUE)
|
||||
dir.create(AzureR_dir, recursive=TRUE)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#' Data directory for AzureRMR
|
||||
#'
|
||||
#' @details
|
||||
#' AzureRMR can store authentication credentials and OAuth tokens in a user-specific directory, using the rappdirs package. On recent Windows versions, this will usually be in the location `C:\\Users\\(username)\\AppData\\Local\\AzureR\\AzureRMR`. On Unix/Linux, it will be in `~/.local/share/AzureRMR`, and on MacOS, it will be in `~/Library/Application Support/AzureRMR`. The working directory is not touched (which significantly lessens the risk of accidentally introducing cached tokens into source control).
|
||||
#'
|
||||
#' On package startup, if this directory does not exist, AzureRMR will prompt you for permission to create it. It's recommended that you allow the directory to be created, as otherwise you will have to reauthenticate with Azure every time. Note that many cloud engineering tools, including the [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/?view=azure-cli-latest), save authentication credentials in this way.
|
||||
#'
|
||||
#' @return
|
||||
#' A string containing the data directory.
|
||||
#'
|
||||
#' @seealso
|
||||
#' [get_azure_token], [get_azure_login]
|
||||
#'
|
||||
#' [rappdirs::user_data_dir]
|
||||
#'
|
||||
#' @export
|
||||
AzureRMR_dir <- function()
|
||||
{
|
||||
rappdirs::user_data_dir(appname="AzureRMR", appauthor="AzureR", roaming=FALSE)
|
||||
}
|
||||
|
|
520
R/AzureToken.R
520
R/AzureToken.R
|
@ -1,520 +0,0 @@
|
|||
#' Azure OAuth authentication
|
||||
#'
|
||||
#' Azure OAuth 2.0 token class, inheriting from the [Token2.0 class][httr::Token2.0] in httr. Rather than calling the initialization method directly, tokens should be created via [get_azure_token()].
|
||||
#'
|
||||
#' @docType class
|
||||
#' @section Methods:
|
||||
#' - `refresh`: Refreshes the token. For expired Azure tokens using client credentials, refreshing really means requesting a new token.
|
||||
#' - `validate`: Checks if the token is still valid. For Azure tokens using client credentials, this just checks if the current time is less than the token's expiry time.
|
||||
#' - `hash`: Computes an MD5 hash on selected fields of the token. Used internally for identification purposes when caching.
|
||||
#' - `cache`: Stores the token on disk for use in future sessions.
|
||||
#'
|
||||
#' @seealso
|
||||
#' [get_azure_token], [httr::Token]
|
||||
#'
|
||||
#' @format An R6 object of class `AzureToken`.
|
||||
#' @export
|
||||
AzureToken <- R6::R6Class("AzureToken", inherit=httr::Token2.0,
|
||||
|
||||
public=list(
|
||||
|
||||
# need to do hacky init to support explicit re-authentication instead of using a refresh token
|
||||
initialize=function(endpoint, app, user_params, use_device=FALSE, client_credentials=TRUE)
|
||||
{
|
||||
params <- list(scope=NULL, user_params=user_params, type=NULL, use_oob=FALSE, as_header=TRUE,
|
||||
use_basic_auth=FALSE, config_init=list(),
|
||||
client_credentials=client_credentials, use_device=use_device)
|
||||
|
||||
# if this is an existing object, don't use cached value
|
||||
# avoids infinite loop when refresh() calls initialize()
|
||||
tokenfile <- file.path(AzureRMR_dir(), token_hash(endpoint, app, params))
|
||||
if(file.exists(tokenfile) && !isTRUE(private$initialized))
|
||||
{
|
||||
message("Loading cached token")
|
||||
token <- readRDS(tokenfile)
|
||||
self$app <- token$app
|
||||
self$endpoint <- token$endpoint
|
||||
self$params <- token$params
|
||||
self$cache_path <- token$cache_path
|
||||
self$private_key <- token$private_key
|
||||
self$credentials <- token$credentials
|
||||
private$initialized <- TRUE
|
||||
return(self$refresh())
|
||||
}
|
||||
private$initialized <- TRUE
|
||||
|
||||
# use httr initialize for authorization_code, client_credentials methods
|
||||
if(!use_device && is.null(user_params$username))
|
||||
return(super$initialize(app=app, endpoint=endpoint, params=params, cache_path=FALSE))
|
||||
|
||||
self$app <- app
|
||||
self$endpoint <- endpoint
|
||||
self$params <- params
|
||||
self$cache_path <- NULL
|
||||
self$private_key <- NULL
|
||||
|
||||
# use our own init functions for device_code, resource_owner methods
|
||||
if(use_device)
|
||||
private$init_with_device(user_params)
|
||||
else private$init_with_username(user_params)
|
||||
|
||||
if(dir.exists(AzureRMR_dir()))
|
||||
saveRDS(self, tokenfile)
|
||||
|
||||
self
|
||||
},
|
||||
|
||||
# overrides httr::Token method
|
||||
hash=function()
|
||||
{
|
||||
token_hash(self$endpoint, self$app, self$params)
|
||||
},
|
||||
|
||||
# overrides httr::Token method
|
||||
cache=function()
|
||||
{
|
||||
if(dir.exists(AzureRMR_dir()))
|
||||
{
|
||||
filename <- file.path(AzureRMR_dir(), self$hash())
|
||||
saveRDS(self, filename)
|
||||
}
|
||||
invisible(NULL)
|
||||
},
|
||||
|
||||
# overrides httr::Token2.0 method
|
||||
can_refresh=function()
|
||||
{
|
||||
TRUE # always can refresh
|
||||
},
|
||||
|
||||
# overrides httr::Token2.0 method
|
||||
validate=function()
|
||||
{
|
||||
if(!is.null(self$endpoint$validate))
|
||||
return(super$validate())
|
||||
|
||||
expdate <- as.POSIXct(as.numeric(self$credentials$expires_on), origin="1970-01-01")
|
||||
curdate <- Sys.time()
|
||||
curdate < expdate
|
||||
},
|
||||
|
||||
# overrides httr::Token2.0 method
|
||||
refresh=function()
|
||||
{
|
||||
# use a refresh token if it exists
|
||||
# don't call superclass method b/c of different caching logic
|
||||
if(!is.null(self$credentials$refresh_token))
|
||||
{
|
||||
body <- list(
|
||||
refresh_token=self$credentials$refresh_token,
|
||||
client_id=self$app$key,
|
||||
client_secret=self$app$secret,
|
||||
grant_type="refresh_token"
|
||||
)
|
||||
body <- modifyList(body, self$params$user_params)
|
||||
|
||||
access_uri <- sub("devicecode$", "token", self$endpoint$access)
|
||||
res <- httr::POST(access_uri, body=body, encode="form")
|
||||
|
||||
if(httr::status_code(res) >= 300)
|
||||
{
|
||||
delete_azure_token(hash=self$hash(), confirm=FALSE)
|
||||
stop("Unable to refresh", call.=FALSE)
|
||||
}
|
||||
self$credentials <- utils::modifyList(self$credentials, httr::content(res))
|
||||
}
|
||||
else # re-authenticate if no refresh token
|
||||
{
|
||||
# save the hash so we can delete the cached token on failure (initialize can modify state)
|
||||
hash <- self$hash()
|
||||
|
||||
res <- try(self$initialize(self$endpoint, self$app, self$params$user_params,
|
||||
use_device=self$params$use_device,
|
||||
client_credentials=self$params$client_credentials), silent=TRUE)
|
||||
if(inherits(res, "try-error"))
|
||||
{
|
||||
delete_azure_token(hash=hash, confirm=FALSE)
|
||||
stop("Unable to reauthenticate", call.=FALSE)
|
||||
}
|
||||
}
|
||||
|
||||
self$cache()
|
||||
self
|
||||
},
|
||||
|
||||
print=function()
|
||||
{
|
||||
cat(format_auth_header(self))
|
||||
invisible(self)
|
||||
}
|
||||
),
|
||||
|
||||
private=list(
|
||||
initialized=NULL,
|
||||
|
||||
# device code authentication: after sending initial request, loop until server indicates code has been received
|
||||
# after init_oauth2.0, oauth2.0_access_token
|
||||
init_with_device=function(user_params)
|
||||
{
|
||||
# must be in an interactive session to use devicecode; should not affect cached tokens
|
||||
if(!interactive())
|
||||
stop("Must be in an interactive session to use device code authentication", call.=FALSE)
|
||||
|
||||
creds <- httr::oauth2.0_access_token(self$endpoint, self$app, code=NULL, user_params=user_params,
|
||||
redirect_uri=NULL)
|
||||
|
||||
cat(creds$message, "\n") # tell user to enter the code
|
||||
|
||||
req_params <- list(client_id=self$app$key, grant_type="device_code", code=creds$device_code)
|
||||
req_params <- utils::modifyList(user_params, req_params)
|
||||
access_uri <- sub("devicecode$", "token", self$endpoint$access)
|
||||
|
||||
message("Waiting for device code in browser...\nPress Esc/Ctrl + C to abort")
|
||||
interval <- as.numeric(creds$interval)
|
||||
ntries <- as.numeric(creds$expires_in) %/% interval
|
||||
for(i in seq_len(ntries))
|
||||
{
|
||||
Sys.sleep(interval)
|
||||
|
||||
res <- httr::POST(access_uri, httr::add_headers(`Cache-Control`="no-cache"), encode="form",
|
||||
body=req_params)
|
||||
|
||||
status <- httr::status_code(res)
|
||||
cont <- httr::content(res)
|
||||
if(status == 400 && cont$error == "authorization_pending")
|
||||
{
|
||||
# do nothing
|
||||
}
|
||||
else if(status >= 300)
|
||||
httr::stop_for_status(res)
|
||||
else break
|
||||
}
|
||||
if(status >= 300)
|
||||
stop("Unable to authenticate")
|
||||
|
||||
message("Authentication complete.")
|
||||
self$credentials <- cont
|
||||
NULL
|
||||
},
|
||||
|
||||
# resource owner authentication: send username/password
|
||||
init_with_username=function(user_params)
|
||||
{
|
||||
body <- list(
|
||||
resource=user_params$resource,
|
||||
client_id=self$app$key,
|
||||
grant_type="password",
|
||||
username=user_params$username,
|
||||
password=user_params$password)
|
||||
|
||||
res <- httr::POST(self$endpoint$access, httr::add_headers(`Cache-Control`="no-cache"), encode="form",
|
||||
body=body)
|
||||
|
||||
httr::stop_for_status(res, task="get an access token")
|
||||
self$credentials <- httr::content(res)
|
||||
NULL
|
||||
}
|
||||
))
|
||||
|
||||
|
||||
#' Manage Azure Active Directory OAuth 2.0 tokens
|
||||
#'
|
||||
#' These functions extend the OAuth functionality in httr for use with Azure Active Directory (AAD).
|
||||
#'
|
||||
#' @param resource URL for your resource host. For Resource Manager in the public Azure cloud, this is `https://management.azure.com/`.
|
||||
#' @param tenant Your tenant. This can be a name ("myaadtenant"), a fully qualified domain name ("myaadtenant.onmicrosoft.com" or "mycompanyname.com"), or a GUID.
|
||||
#' @param app The client/app ID to use to authenticate with.
|
||||
#' @param password The password, either for the app, or your username if supplied. See 'Details' below.
|
||||
#' @param username Your AAD username, if using the resource owner grant. See 'Details' below.
|
||||
#' @param auth_type The authentication type. See 'Details' below.
|
||||
#' @param aad_host URL for your AAD host. For the public Azure cloud, this is `https://login.microsoftonline.com/`. Change this if you are using a government or private cloud.
|
||||
#'
|
||||
#' @details
|
||||
#' `get_azure_token` does much the same thing as [httr::oauth2.0_token()], but customised for Azure. It obtains an OAuth token, first by checking if a cached value exists on disk, and if not, acquiring it from the AAD server. `delete_azure_token` deletes a cached token, and `list_azure_tokens` lists currently cached tokens.
|
||||
#'
|
||||
#' Note that tokens are only cached if you allowed AzureRMR to create a data directory at package startup.
|
||||
#'
|
||||
#' @section Authentication methods:
|
||||
#' The OAuth authentication type can be one of four possible values: "authorization_code", "client_credentials", "device_code", or "resource_owner". The first two are provided by the [httr::Token2.0] token class, while the last two are provided by the AzureToken class which extends httr::Token2.0. Here is a short description of these methods.
|
||||
#'
|
||||
#' 1. Using the authorization_code method is a 3-step process. First, `get_azure_token` contacts the AAD authorization endpoint to obtain a temporary access code. It then contacts the AAD access endpoint, passing it the code. The access endpoint sends back a login URL which `get_azure_token` opens in your browser, where you can enter your credentials. Once this is completed, the endpoint returns the OAuth token via a HTTP redirect URI.
|
||||
#'
|
||||
#' 2. The device_code method is similar in concept to authorization_code, but is meant for situations where you are unable to browse the Internet -- for example if you don't have a browser installed or your computer has input constraints. First, `get_azure_token` contacts the AAD devicecode endpoint, which responds with a login URL and an access code. You then visit the URL and enter the code, possibly using a different computer. Meanwhile, `get_azure_token` polls the AAD access endpoint for a token, which is provided once you have successfully entered the code.
|
||||
#'
|
||||
#' 3. The client_credentials method is much simpler than the above methods, requiring only one step. `get_azure_token` contacts the access endpoint, passing it the app secret (which you supplied in the `password` argument). Assuming the secret is valid, the endpoint then returns the OAuth token.
|
||||
#'
|
||||
#' 4. The resource_owner method also requires only one step. In this method, `get_azure_token` passes your (personal) username and password to the AAD access endpoint, which validates your credentials and returns the token.
|
||||
#'
|
||||
#' If the authentication method is not specified, it is chosen based on the presence or absence of the `password` and `username` arguments:
|
||||
#'
|
||||
#' - Password and username present: resource_owner.
|
||||
#' - Password and username absent: authorization_code if the httpuv package is installed, device_code otherwise
|
||||
#' - Password present, username absent: client_credentials
|
||||
#' - Password absent, username present: error
|
||||
#'
|
||||
#' The httpuv package must be installed to use the authorization_code method, as this requires a web server to listen on the (local) redirect URI. See [httr::oauth2.0_token] for more information; note that Azure does not support the `use_oob` feature of the httr OAuth 2.0 token class.
|
||||
#'
|
||||
#' Similarly, since the authorization_code method opens a browser to load the AAD authorization page, your machine must have an Internet browser installed that can be run from inside R. In particular, if you are using a Linux [Data Science Virtual Machine](https://azure.microsoft.com/en-us/services/virtual-machines/data-science-virtual-machines/) in Azure, you may run into difficulties; use one of the other methods instead.
|
||||
#'
|
||||
#' @section Caching:
|
||||
#' AzureRMR differs from httr in its handling of token caching in a number of ways. First, caching is based on all the inputs to `get_azure_token` as listed above. Second, it defines its own directory for cached tokens, using the rappdirs package. On recent Windows versions, this will usually be in the location `C:\\Users\\(username)\\AppData\\Local\\AzureR\\AzureRMR`. On Linux, it will be in `~/.config/AzureRMR`, and on MacOS, it will be in `~/Library/Application Support/AzureRMR`. Note that a single directory is used for all tokens, and the working directory is not touched (which significantly lessens the risk of accidentally introducing cached tokens into source control).
|
||||
#'
|
||||
#' To list all cached tokens on disk, use `list_azure_tokens`. This returns a list of token objects, named according to their MD5 hashes.
|
||||
#'
|
||||
#' To delete a cached token, use `delete_azure_token`. This takes the same inputs as `get_azure_token`, or you can specify the MD5 hash directly in the `hash` argument.
|
||||
#'
|
||||
#' To delete _all_ cached tokens, use `clean_token_directory`.
|
||||
#'
|
||||
#' @section Value:
|
||||
#' For `get_azure_token`, an object of class `AzureToken` representing the AAD token. For `list_azure_tokens`, a list of such objects retrieved from disk.
|
||||
#'
|
||||
#' @seealso
|
||||
#' [AzureToken], [httr::oauth2.0_token], [httr::Token],
|
||||
#'
|
||||
#' [OAuth authentication for Azure Active Directory](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-code),
|
||||
#' [Device code flow on OAuth.com](https://www.oauth.com/oauth2-servers/device-flow/token-request/),
|
||||
#' [OAuth 2.0 RFC](https://tools.ietf.org/html/rfc6749) for the gory details on how OAuth works
|
||||
#'
|
||||
#' @examples
|
||||
#' \dontrun{
|
||||
#'
|
||||
#' # authenticate with Azure Resource Manager:
|
||||
#' # no user credentials are supplied, so this will use the authorization_code
|
||||
#' # method if httpuv is installed, and device_code if not
|
||||
#' arm_token <- get_azure_token(
|
||||
#' resource="https://management.azure.com/",
|
||||
#' tenant="myaadtenant.onmicrosoft.com",
|
||||
#' app="app_id")
|
||||
#'
|
||||
#' # you can force a specific authentication method with the auth_type argument
|
||||
#' arm_token <- get_azure_token(
|
||||
#' resource="https://management.azure.com/",
|
||||
#' tenant="myaadtenant.onmicrosoft.com",
|
||||
#' app="app_id",
|
||||
#' auth_type="device_code")
|
||||
#'
|
||||
#' # to use the client_credentials method, supply the app secret as the password
|
||||
#' arm_token <- get_azure_token(
|
||||
#' resource="https://management.azure.com/",
|
||||
#' tenant="myaadtenant.onmicrosoft.com",
|
||||
#' app="app_id",
|
||||
#' password="app_secret")
|
||||
#'
|
||||
#' # authenticate with Azure storage
|
||||
#' storage_token <- get_azure_token(
|
||||
#' resource="https://storage.azure.com/",
|
||||
#' tenant="myaadtenant.onmicrosoft.com",
|
||||
#' app="app_id")
|
||||
#'
|
||||
#' # authenticate to your resource with the resource_owner method: provide your username and password
|
||||
#' owner_token <- get_azure_token(
|
||||
#' resource="https://myresource/",
|
||||
#' tenant="myaadtenant",
|
||||
#' app="app_id",
|
||||
#' username="user",
|
||||
#' password="abcdefg")
|
||||
#'
|
||||
#' # list saved tokens
|
||||
#' list_azure_tokens()
|
||||
#'
|
||||
#' # delete a saved token from disk
|
||||
#' delete_azure_token(
|
||||
#' resource="https://myresource/",
|
||||
#' tenant="myaadtenant",
|
||||
#' app="app_id",
|
||||
#' username="user",
|
||||
#' password="abcdefg")
|
||||
#'
|
||||
#' # delete a saved token by specifying its MD5 hash
|
||||
#' delete_azure_token(hash="7ea491716e5b10a77a673106f3f53bfd")
|
||||
#'
|
||||
#' }
|
||||
#' @export
|
||||
get_azure_token <- function(resource, tenant, app, password=NULL, username=NULL, auth_type=NULL,
|
||||
aad_host="https://login.microsoftonline.com/")
|
||||
{
|
||||
tenant <- normalize_tenant(tenant)
|
||||
app <- normalize_guid(app)
|
||||
base_url <- construct_path(aad_host, tenant)
|
||||
|
||||
if(is.null(auth_type))
|
||||
auth_type <- select_auth_type(password, username)
|
||||
|
||||
# fail if authorization_code selected but httpuv not available
|
||||
if(auth_type == "authorization_code" && system.file(package="httpuv") == "")
|
||||
stop("httpuv package must be installed to use authorization_code method", call.=FALSE)
|
||||
|
||||
switch(auth_type,
|
||||
client_credentials=
|
||||
auth_with_client_creds(base_url, app, password, resource),
|
||||
device_code=
|
||||
auth_with_device(base_url, app, resource),
|
||||
authorization_code=
|
||||
auth_with_code(base_url, app, resource),
|
||||
resource_owner=
|
||||
auth_with_username(base_url, app, password, username, resource),
|
||||
stop("Invalid auth_type argument", call.=FALSE))
|
||||
}
|
||||
|
||||
|
||||
auth_with_client_creds <- function(base_url, app, password, resource)
|
||||
{
|
||||
endp <- httr::oauth_endpoint(base_url=base_url, authorize="oauth2/authorize", access="oauth2/token")
|
||||
app <- httr::oauth_app("azure", key=app, secret=password, redirect_uri=NULL)
|
||||
|
||||
AzureToken$new(endp, app, user_params=list(resource=resource), use_device=FALSE, client_credentials=TRUE)
|
||||
}
|
||||
|
||||
|
||||
auth_with_device <- function(base_url, app, resource)
|
||||
{
|
||||
endp <- httr::oauth_endpoint(base_url=base_url, authorize="oauth2/authorize", access="oauth2/devicecode")
|
||||
app <- httr::oauth_app("azure", key=app, secret=NULL)
|
||||
|
||||
AzureToken$new(endp, app, user_params=list(resource=resource), use_device=TRUE, client_credentials=FALSE)
|
||||
}
|
||||
|
||||
|
||||
auth_with_code <- function(base_url, app, resource)
|
||||
{
|
||||
endp <- httr::oauth_endpoint(base_url=base_url, authorize="oauth2/authorize", access="oauth2/token")
|
||||
app <- httr::oauth_app("azure", key=app, secret=NULL)
|
||||
|
||||
AzureToken$new(endp, app, user_params=list(resource=resource), use_device=FALSE, client_credentials=FALSE)
|
||||
}
|
||||
|
||||
|
||||
auth_with_username <- function(base_url, app, password, username, resource)
|
||||
{
|
||||
endp <- httr::oauth_endpoint(base_url=base_url, authorize="oauth2/authorize", access="oauth2/token")
|
||||
app <- httr::oauth_app("azure", key=app, secret=NULL)
|
||||
|
||||
AzureToken$new(endp, app, user_params=list(resource=resource, username=username, password=password),
|
||||
use_device=FALSE, client_credentials=FALSE)
|
||||
}
|
||||
|
||||
|
||||
# select authentication method based on input arguments and presence of httpuv
|
||||
select_auth_type <- function(password, username)
|
||||
{
|
||||
got_pwd <- !is.null(password)
|
||||
got_user <- !is.null(username)
|
||||
|
||||
if(got_pwd && got_user)
|
||||
"resource_owner"
|
||||
else if(!got_pwd && !got_user)
|
||||
{
|
||||
if(system.file(package="httpuv") == "")
|
||||
{
|
||||
message("httpuv not installed, defaulting to device code authentication")
|
||||
"device_code"
|
||||
}
|
||||
else "authorization_code"
|
||||
}
|
||||
else if(got_pwd && !got_user)
|
||||
"client_credentials"
|
||||
else stop("Can't select authentication method", call.=FALSE)
|
||||
}
|
||||
|
||||
|
||||
#' @param hash The MD5 hash of this token, computed from the above inputs. Used by `delete_azure_token` to identify a cached token to delete.
|
||||
#' @param confirm For `delete_azure_token`, whether to prompt for confirmation before deleting a token.
|
||||
#' @rdname get_azure_token
|
||||
#' @export
|
||||
delete_azure_token <- function(resource, tenant, app, password=NULL, username=NULL, auth_type=NULL,
|
||||
aad_host="https://login.microsoftonline.com/",
|
||||
hash=NULL,
|
||||
confirm=TRUE)
|
||||
{
|
||||
if(!dir.exists(AzureRMR_dir()))
|
||||
return(invisible(NULL))
|
||||
|
||||
if(is.null(hash))
|
||||
hash <- token_hash_from_original_args(resource, tenant, app, password, username, auth_type, aad_host)
|
||||
|
||||
if(confirm && interactive())
|
||||
{
|
||||
yn <- readline(paste0("Do you really want to delete this Azure Active Directory token? (y/N) "))
|
||||
if(tolower(substr(yn, 1, 1)) != "y")
|
||||
return(invisible(NULL))
|
||||
}
|
||||
file.remove(file.path(AzureRMR_dir(), hash))
|
||||
invisible(NULL)
|
||||
}
|
||||
|
||||
|
||||
#' @rdname get_azure_token
|
||||
#' @export
|
||||
clean_token_directory <- function(confirm=TRUE)
|
||||
{
|
||||
if(!dir.exists(AzureRMR_dir()))
|
||||
return(invisible(NULL))
|
||||
|
||||
if(confirm && interactive())
|
||||
{
|
||||
yn <- readline(paste0("Do you really want to delete ALL saved Azure Active Directory tokens? (y/N) "))
|
||||
if(tolower(substr(yn, 1, 1)) != "y")
|
||||
return(invisible(NULL))
|
||||
}
|
||||
toks <- dir(AzureRMR_dir(), pattern="^[0-9a-f]{32}$", full.names=TRUE)
|
||||
file.remove(toks)
|
||||
invisible(NULL)
|
||||
}
|
||||
|
||||
|
||||
#' @rdname get_azure_token
|
||||
#' @export
|
||||
list_azure_tokens <- function()
|
||||
{
|
||||
tokens <- dir(AzureRMR_dir(), pattern="[0-9a-f]{32}", full.names=TRUE)
|
||||
lst <- lapply(tokens, function(fname)
|
||||
{
|
||||
x <- try(readRDS(fname), silent=TRUE)
|
||||
if(is_azure_token(x))
|
||||
x
|
||||
else NULL
|
||||
})
|
||||
names(lst) <- basename(tokens)
|
||||
lst[!sapply(lst, is.null)]
|
||||
}
|
||||
|
||||
|
||||
token_hash <- function(endpoint, app, params)
|
||||
{
|
||||
msg <- serialize(list(endpoint, app, params), NULL, version=2)
|
||||
paste(openssl::md5(msg[-(1:14)]), collapse="")
|
||||
}
|
||||
|
||||
|
||||
token_hash_from_original_args <- function(resource, tenant, app, password=NULL, username=NULL, auth_type=NULL,
|
||||
aad_host="https://login.microsoftonline.com/")
|
||||
{
|
||||
# reconstruct the hash for the token object from the inputs
|
||||
tenant <- normalize_tenant(tenant)
|
||||
app <- normalize_guid(app)
|
||||
base_url <- construct_path(aad_host, tenant)
|
||||
|
||||
if(is.null(auth_type))
|
||||
auth_type <- select_auth_type(password, username)
|
||||
|
||||
base_url <- construct_path(aad_host, tenant)
|
||||
use_device <- auth_type == "device_code"
|
||||
client_credentials <- auth_type == "client_credentials"
|
||||
|
||||
endp <- httr::oauth_endpoint(base_url=base_url,
|
||||
authorize="oauth2/authorize",
|
||||
access=if(use_device) "oauth2/devicecode" else "oauth2/token")
|
||||
app <- httr::oauth_app("azure", app,
|
||||
secret=if(client_credentials) password else NULL,
|
||||
redirect_uri=if(client_credentials) NULL else httr::oauth_callback())
|
||||
|
||||
user_params <- list(resource=resource)
|
||||
if(auth_type == "resource_owner")
|
||||
user_params <- c(user_params, password=NULL, username=NULL)
|
||||
|
||||
params <- list(scope=NULL, user_params=user_params, type=NULL, use_oob=FALSE, as_header=TRUE,
|
||||
use_basic_auth=FALSE, config_init=list(),
|
||||
client_credentials=client_credentials, use_device=use_device)
|
||||
|
||||
token_hash(endp, app, params)
|
||||
}
|
22
R/az_login.R
22
R/az_login.R
|
@ -50,7 +50,7 @@
|
|||
#' If the AzureRMR data directory for saving credentials does not exist, `get_azure_login` will throw an error.
|
||||
#'
|
||||
#' @seealso
|
||||
#' [az_rm], [get_azure_token],
|
||||
#' [az_rm], [AzureAuth::get_azure_token],
|
||||
#'
|
||||
#' [Azure Resource Manager overview](https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-overview),
|
||||
#' [REST API reference](https://docs.microsoft.com/en-us/rest/api/resources/)
|
||||
|
@ -91,7 +91,7 @@ create_azure_login <- function(tenant, app=.az_cli_app_id, password=NULL, userna
|
|||
if(!is.null(conf$aad_host)) aad_host <- conf$aad_host
|
||||
}
|
||||
|
||||
hash <- token_hash_from_original_args(
|
||||
hash <- token_hash(
|
||||
resource=host,
|
||||
tenant=tenant,
|
||||
app=app,
|
||||
|
@ -100,7 +100,7 @@ create_azure_login <- function(tenant, app=.az_cli_app_id, password=NULL, userna
|
|||
auth_type=auth_type,
|
||||
aad_host=aad_host
|
||||
)
|
||||
tokenfile <- file.path(AzureRMR_dir(), hash)
|
||||
tokenfile <- file.path(AzureR_dir(), hash)
|
||||
if(file.exists(tokenfile))
|
||||
{
|
||||
message("Deleting existing Azure Active Directory token for this set of credentials")
|
||||
|
@ -126,7 +126,7 @@ create_azure_login <- function(tenant, app=.az_cli_app_id, password=NULL, userna
|
|||
#' @export
|
||||
get_azure_login <- function(tenant, selection=NULL, refresh=TRUE)
|
||||
{
|
||||
if(!dir.exists(AzureRMR_dir()))
|
||||
if(!dir.exists(AzureR_dir()))
|
||||
stop("AzureRMR data directory does not exist; cannot load saved logins")
|
||||
|
||||
tenant <- normalize_tenant(tenant)
|
||||
|
@ -142,7 +142,7 @@ get_azure_login <- function(tenant, selection=NULL, refresh=TRUE)
|
|||
else if(is.null(selection))
|
||||
{
|
||||
tokens <- lapply(this_login, function(f)
|
||||
readRDS(file.path(AzureRMR_dir(), f)))
|
||||
readRDS(file.path(AzureR_dir(), f)))
|
||||
|
||||
choices <- sapply(tokens, function(token)
|
||||
{
|
||||
|
@ -169,7 +169,7 @@ get_azure_login <- function(tenant, selection=NULL, refresh=TRUE)
|
|||
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(AzureRMR_dir(), file)
|
||||
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)
|
||||
|
||||
|
@ -188,7 +188,7 @@ get_azure_login <- function(tenant, selection=NULL, refresh=TRUE)
|
|||
#' @export
|
||||
delete_azure_login <- function(tenant, confirm=TRUE)
|
||||
{
|
||||
if(!dir.exists(AzureRMR_dir()))
|
||||
if(!dir.exists(AzureR_dir()))
|
||||
{
|
||||
warning("AzureRMR data directory does not exist; no logins to delete")
|
||||
return(invisible(NULL))
|
||||
|
@ -221,7 +221,7 @@ list_azure_logins <- function()
|
|||
{
|
||||
sapply(tenant, function(hash)
|
||||
{
|
||||
file <- file.path(AzureRMR_dir(), hash)
|
||||
file <- file.path(AzureR_dir(), hash)
|
||||
az_rm$new(token=readRDS(file))
|
||||
}, simplify=FALSE)
|
||||
}, simplify=FALSE)
|
||||
|
@ -232,7 +232,7 @@ list_azure_logins <- function()
|
|||
|
||||
load_arm_logins <- function()
|
||||
{
|
||||
file <- file.path(AzureRMR_dir(), "arm_logins.json")
|
||||
file <- file.path(AzureR_dir(), "arm_logins.json")
|
||||
if(!file.exists(file))
|
||||
return(structure(list(), names=character(0)))
|
||||
jsonlite::fromJSON(file)
|
||||
|
@ -241,7 +241,7 @@ load_arm_logins <- function()
|
|||
|
||||
save_arm_logins <- function(logins)
|
||||
{
|
||||
if(!dir.exists(AzureRMR_dir()))
|
||||
if(!dir.exists(AzureR_dir()))
|
||||
{
|
||||
message("AzureRMR data directory does not exist; login credentials not saved")
|
||||
return(invisible(NULL))
|
||||
|
@ -250,7 +250,7 @@ save_arm_logins <- function(logins)
|
|||
if(is_empty(logins))
|
||||
names(logins) <- character(0)
|
||||
|
||||
file <- file.path(AzureRMR_dir(), "arm_logins.json")
|
||||
file <- file.path(AzureR_dir(), "arm_logins.json")
|
||||
writeLines(jsonlite::toJSON(logins, auto_unbox=TRUE, pretty=TRUE), file)
|
||||
invisible(NULL)
|
||||
}
|
||||
|
|
7
R/is.R
7
R/is.R
|
@ -45,10 +45,3 @@ is_template <- function(object)
|
|||
R6::is.R6(object) && inherits(object, "az_template")
|
||||
}
|
||||
|
||||
|
||||
#' @rdname is
|
||||
#' @export
|
||||
is_azure_token <- function(object)
|
||||
{
|
||||
R6::is.R6(object) && inherits(object, "AzureToken")
|
||||
}
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
#' Normalize GUID and tenant values
|
||||
#'
|
||||
#' These functions are used by `get_azure_token` to recognise and properly format tenant and app IDs.
|
||||
#'
|
||||
#' @param tenant For `normalize_tenant`, a string containing an Azure Active Directory tenant. This can be a name ("myaadtenant"), a fully qualified domain name ("myaadtenant.onmicrosoft.com" or "mycompanyname.com"), or a valid GUID.
|
||||
#' @param x For `is_guid`, a character string; for `normalize_guid`, a string containing a _validly formatted_ GUID.
|
||||
#'
|
||||
#' @details
|
||||
#' A tenant can be identified either by a GUID, or its name, or a fully-qualified domain name (FQDN). The rules for normalizing a tenant are:
|
||||
#' 1. If `tenant` is recognised as a valid GUID, return its canonically formatted value
|
||||
#' 2. Otherwise, if it is a FQDN, return it
|
||||
#' 3. Otherwise, if it is not the string "common", append ".onmicrosoft.com" to it
|
||||
#' 4. Otherwise, return the value of `tenant`
|
||||
#'
|
||||
#' See the link below for GUID formats recognised by these functions.
|
||||
#'
|
||||
#' @return
|
||||
#' For `is_guid`, whether the argument is a validly formatted GUID.
|
||||
#'
|
||||
#' For `normalize_guid`, the GUID in canonical format. If the argument is not recognised as a GUID, it throws an error.
|
||||
#'
|
||||
#' For `normalize_tenant`, the normalized ID or name of the tenant.
|
||||
#'
|
||||
#' @seealso
|
||||
#' [get_azure_token]
|
||||
#'
|
||||
#' [Parsing rules for GUIDs in .NET](https://docs.microsoft.com/en-us/dotnet/api/system.guid.parse]). `is_guid` and `normalize_guid` recognise the "N", "D", "B" and "P" formats.
|
||||
#'
|
||||
#' @examples
|
||||
#'
|
||||
#' is_guid("72f988bf-86f1-41af-91ab-2d7cd011db47") # TRUE
|
||||
#' is_guid("{72f988bf-86f1-41af-91ab-2d7cd011db47}") # TRUE
|
||||
#' is_guid("72f988bf-86f1-41af-91ab-2d7cd011db47}") # FALSE (unmatched brace)
|
||||
#' is_guid("microsoft") # FALSE
|
||||
#'
|
||||
#' # all of these return the same value
|
||||
#' normalize_guid("72f988bf-86f1-41af-91ab-2d7cd011db47")
|
||||
#' normalize_guid("{72f988bf-86f1-41af-91ab-2d7cd011db47}")
|
||||
#' normalize_guid("(72f988bf-86f1-41af-91ab-2d7cd011db47)")
|
||||
#' normalize_guid("72f988bf86f141af91ab2d7cd011db47")
|
||||
#'
|
||||
#' normalize_tenant("microsoft") # returns 'microsoft.onmicrosoft.com'
|
||||
#' normalize_tenant("microsoft.com") # returns 'microsoft.com'
|
||||
#' normalize_tenant("72f988bf-86f1-41af-91ab-2d7cd011db47") # returns the GUID
|
||||
#'
|
||||
#' @export
|
||||
#' @rdname guid
|
||||
normalize_tenant <- function(tenant)
|
||||
{
|
||||
# check if supplied a guid; if not, check if a fqdn;
|
||||
# if not, check if 'common'; if not, append '.onmicrosoft.com'
|
||||
if(is_guid(tenant))
|
||||
return(normalize_guid(tenant))
|
||||
|
||||
if(!grepl("\\.", tenant) && tenant != "common")
|
||||
tenant <- paste(tenant, "onmicrosoft.com", sep=".")
|
||||
tenant
|
||||
}
|
||||
|
||||
|
||||
#' @export
|
||||
#' @rdname guid
|
||||
normalize_guid <- function(x)
|
||||
{
|
||||
if(!is_guid(x))
|
||||
stop("Not a GUID", call.=FALSE)
|
||||
|
||||
x <- sub("^[({]?([-0-9a-f]+)[})]$", "\\1", x)
|
||||
x <- gsub("-", "", x)
|
||||
return(paste(
|
||||
substr(x, 1, 8),
|
||||
substr(x, 9, 12),
|
||||
substr(x, 13, 16),
|
||||
substr(x, 17, 20),
|
||||
substr(x, 21, 32), sep="-"))
|
||||
}
|
||||
|
||||
|
||||
#' @export
|
||||
#' @rdname guid
|
||||
is_guid <- function(x)
|
||||
{
|
||||
grepl("^[0-9a-f]{32}$", x) ||
|
||||
grepl("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", x) ||
|
||||
grepl("^\\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}$", x) ||
|
||||
grepl("^\\([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\)$", x)
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
#' @export
|
||||
AzureAuth::clean_token_directory
|
||||
|
||||
#' @export
|
||||
AzureAuth::delete_azure_token
|
||||
|
||||
#' @export
|
||||
AzureAuth::get_azure_token
|
||||
|
||||
#' @export
|
||||
AzureAuth::is_azure_token
|
||||
|
||||
#' @export
|
||||
AzureAuth::is_guid
|
||||
|
||||
#' @export
|
||||
AzureAuth::list_azure_tokens
|
||||
|
||||
#' @export
|
||||
AzureAuth::AzureR_dir
|
|
@ -4,7 +4,7 @@
|
|||
![Downloads](https://cranlogs.r-pkg.org/badges/AzureRMR)
|
||||
[![Travis Build Status](https://travis-ci.org/cloudyr/AzureRMR.png?branch=master)](https://travis-ci.org/cloudyr/AzureRMR)
|
||||
|
||||
AzureRMR is a package for interacting with Azure Active Directory and Azure Resource Manager: obtain AAD authentication tokens, list subscriptions, manage resource groups, deploy and delete templates and resources. It calls the Resource Manager [REST API](https://docs.microsoft.com/en-us/rest/api/resources) directly, so you don't need to have PowerShell or Python installed.
|
||||
AzureRMR is a package for interacting with Azure Resource Manager: list subscriptions, manage resource groups, deploy and delete templates and resources. It calls the Resource Manager [REST API](https://docs.microsoft.com/en-us/rest/api/resources) directly, so you don't need to have PowerShell or Python installed. Azure Active Directory OAuth tokens are obtained using the [AzureAuth](https://github.com/cloudyr/AzureAuth) package.
|
||||
|
||||
You can install the development version from GitHub, via `devtools::install_github("cloudyr/AzureRMR")`.
|
||||
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/AzureRMR.R
|
||||
\name{AzureRMR_dir}
|
||||
\alias{AzureRMR_dir}
|
||||
\title{Data directory for AzureRMR}
|
||||
\usage{
|
||||
AzureRMR_dir()
|
||||
}
|
||||
\value{
|
||||
A string containing the data directory.
|
||||
}
|
||||
\description{
|
||||
Data directory for AzureRMR
|
||||
}
|
||||
\details{
|
||||
AzureRMR can store authentication credentials and OAuth tokens in a user-specific directory, using the rappdirs package. On recent Windows versions, this will usually be in the location \code{C:\\Users\\(username)\\AppData\\Local\\AzureR\\AzureRMR}. On Unix/Linux, it will be in \code{~/.local/share/AzureRMR}, and on MacOS, it will be in \code{~/Library/Application Support/AzureRMR}. The working directory is not touched (which significantly lessens the risk of accidentally introducing cached tokens into source control).
|
||||
|
||||
On package startup, if this directory does not exist, AzureRMR will prompt you for permission to create it. It's recommended that you allow the directory to be created, as otherwise you will have to reauthenticate with Azure every time. Note that many cloud engineering tools, including the \href{https://docs.microsoft.com/en-us/cli/azure/?view=azure-cli-latest}{Azure CLI}, save authentication credentials in this way.
|
||||
}
|
||||
\seealso{
|
||||
\link{get_azure_token}, \link{get_azure_login}
|
||||
|
||||
\link[rappdirs:user_data_dir]{rappdirs::user_data_dir}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/AzureToken.R
|
||||
\docType{class}
|
||||
\name{AzureToken}
|
||||
\alias{AzureToken}
|
||||
\title{Azure OAuth authentication}
|
||||
\format{An R6 object of class \code{AzureToken}.}
|
||||
\usage{
|
||||
AzureToken
|
||||
}
|
||||
\description{
|
||||
Azure OAuth 2.0 token class, inheriting from the \link[httr:Token2.0]{Token2.0 class} in httr. Rather than calling the initialization method directly, tokens should be created via \code{\link[=get_azure_token]{get_azure_token()}}.
|
||||
}
|
||||
\section{Methods}{
|
||||
|
||||
\itemize{
|
||||
\item \code{refresh}: Refreshes the token. For expired Azure tokens using client credentials, refreshing really means requesting a new token.
|
||||
\item \code{validate}: Checks if the token is still valid. For Azure tokens using client credentials, this just checks if the current time is less than the token's expiry time.
|
||||
\item \code{hash}: Computes an MD5 hash on selected fields of the token. Used internally for identification purposes when caching.
|
||||
\item \code{cache}: Stores the token on disk for use in future sessions.
|
||||
}
|
||||
}
|
||||
|
||||
\seealso{
|
||||
\link{get_azure_token}, \link[httr:Token]{httr::Token}
|
||||
}
|
||||
\keyword{datasets}
|
|
@ -103,7 +103,7 @@ az <- get_azure_login("myaadtenant")
|
|||
}
|
||||
}
|
||||
\seealso{
|
||||
\link{az_rm}, \link{get_azure_token},
|
||||
\link{az_rm}, \link[AzureAuth:get_azure_token]{AzureAuth::get_azure_token},
|
||||
|
||||
\href{https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-overview}{Azure Resource Manager overview},
|
||||
\href{https://docs.microsoft.com/en-us/rest/api/resources/}{REST API reference}
|
||||
|
|
|
@ -1,150 +0,0 @@
|
|||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/AzureToken.R
|
||||
\name{get_azure_token}
|
||||
\alias{get_azure_token}
|
||||
\alias{delete_azure_token}
|
||||
\alias{clean_token_directory}
|
||||
\alias{list_azure_tokens}
|
||||
\title{Manage Azure Active Directory OAuth 2.0 tokens}
|
||||
\usage{
|
||||
get_azure_token(resource, tenant, app, password = NULL,
|
||||
username = NULL, auth_type = NULL,
|
||||
aad_host = "https://login.microsoftonline.com/")
|
||||
|
||||
delete_azure_token(resource, tenant, app, password = NULL,
|
||||
username = NULL, auth_type = NULL,
|
||||
aad_host = "https://login.microsoftonline.com/", hash = NULL,
|
||||
confirm = TRUE)
|
||||
|
||||
clean_token_directory(confirm = TRUE)
|
||||
|
||||
list_azure_tokens()
|
||||
}
|
||||
\arguments{
|
||||
\item{resource}{URL for your resource host. For Resource Manager in the public Azure cloud, this is \code{https://management.azure.com/}.}
|
||||
|
||||
\item{tenant}{Your tenant. This can be a name ("myaadtenant"), a fully qualified domain name ("myaadtenant.onmicrosoft.com" or "mycompanyname.com"), or a GUID.}
|
||||
|
||||
\item{app}{The client/app ID to use to authenticate with.}
|
||||
|
||||
\item{password}{The password, either for the app, or your username if supplied. See 'Details' below.}
|
||||
|
||||
\item{username}{Your AAD username, if using the resource owner grant. See 'Details' below.}
|
||||
|
||||
\item{auth_type}{The authentication type. See 'Details' below.}
|
||||
|
||||
\item{aad_host}{URL for your AAD host. For the public Azure cloud, this is \code{https://login.microsoftonline.com/}. Change this if you are using a government or private cloud.}
|
||||
|
||||
\item{hash}{The MD5 hash of this token, computed from the above inputs. Used by \code{delete_azure_token} to identify a cached token to delete.}
|
||||
|
||||
\item{confirm}{For \code{delete_azure_token}, whether to prompt for confirmation before deleting a token.}
|
||||
}
|
||||
\description{
|
||||
These functions extend the OAuth functionality in httr for use with Azure Active Directory (AAD).
|
||||
}
|
||||
\details{
|
||||
\code{get_azure_token} does much the same thing as \code{\link[httr:oauth2.0_token]{httr::oauth2.0_token()}}, but customised for Azure. It obtains an OAuth token, first by checking if a cached value exists on disk, and if not, acquiring it from the AAD server. \code{delete_azure_token} deletes a cached token, and \code{list_azure_tokens} lists currently cached tokens.
|
||||
|
||||
Note that tokens are only cached if you allowed AzureRMR to create a data directory at package startup.
|
||||
}
|
||||
\section{Authentication methods}{
|
||||
|
||||
The OAuth authentication type can be one of four possible values: "authorization_code", "client_credentials", "device_code", or "resource_owner". The first two are provided by the \link[httr:Token2.0]{httr::Token2.0} token class, while the last two are provided by the AzureToken class which extends httr::Token2.0. Here is a short description of these methods.
|
||||
\enumerate{
|
||||
\item Using the authorization_code method is a 3-step process. First, \code{get_azure_token} contacts the AAD authorization endpoint to obtain a temporary access code. It then contacts the AAD access endpoint, passing it the code. The access endpoint sends back a login URL which \code{get_azure_token} opens in your browser, where you can enter your credentials. Once this is completed, the endpoint returns the OAuth token via a HTTP redirect URI.
|
||||
\item The device_code method is similar in concept to authorization_code, but is meant for situations where you are unable to browse the Internet -- for example if you don't have a browser installed or your computer has input constraints. First, \code{get_azure_token} contacts the AAD devicecode endpoint, which responds with a login URL and an access code. You then visit the URL and enter the code, possibly using a different computer. Meanwhile, \code{get_azure_token} polls the AAD access endpoint for a token, which is provided once you have successfully entered the code.
|
||||
\item The client_credentials method is much simpler than the above methods, requiring only one step. \code{get_azure_token} contacts the access endpoint, passing it the app secret (which you supplied in the \code{password} argument). Assuming the secret is valid, the endpoint then returns the OAuth token.
|
||||
\item The resource_owner method also requires only one step. In this method, \code{get_azure_token} passes your (personal) username and password to the AAD access endpoint, which validates your credentials and returns the token.
|
||||
}
|
||||
|
||||
If the authentication method is not specified, it is chosen based on the presence or absence of the \code{password} and \code{username} arguments:
|
||||
\itemize{
|
||||
\item Password and username present: resource_owner.
|
||||
\item Password and username absent: authorization_code if the httpuv package is installed, device_code otherwise
|
||||
\item Password present, username absent: client_credentials
|
||||
\item Password absent, username present: error
|
||||
}
|
||||
|
||||
The httpuv package must be installed to use the authorization_code method, as this requires a web server to listen on the (local) redirect URI. See \link[httr:oauth2.0_token]{httr::oauth2.0_token} for more information; note that Azure does not support the \code{use_oob} feature of the httr OAuth 2.0 token class.
|
||||
|
||||
Similarly, since the authorization_code method opens a browser to load the AAD authorization page, your machine must have an Internet browser installed that can be run from inside R. In particular, if you are using a Linux \href{https://azure.microsoft.com/en-us/services/virtual-machines/data-science-virtual-machines/}{Data Science Virtual Machine} in Azure, you may run into difficulties; use one of the other methods instead.
|
||||
}
|
||||
|
||||
\section{Caching}{
|
||||
|
||||
AzureRMR differs from httr in its handling of token caching in a number of ways. First, caching is based on all the inputs to \code{get_azure_token} as listed above. Second, it defines its own directory for cached tokens, using the rappdirs package. On recent Windows versions, this will usually be in the location \code{C:\\Users\\(username)\\AppData\\Local\\AzureR\\AzureRMR}. On Linux, it will be in \code{~/.config/AzureRMR}, and on MacOS, it will be in \code{~/Library/Application Support/AzureRMR}. Note that a single directory is used for all tokens, and the working directory is not touched (which significantly lessens the risk of accidentally introducing cached tokens into source control).
|
||||
|
||||
To list all cached tokens on disk, use \code{list_azure_tokens}. This returns a list of token objects, named according to their MD5 hashes.
|
||||
|
||||
To delete a cached token, use \code{delete_azure_token}. This takes the same inputs as \code{get_azure_token}, or you can specify the MD5 hash directly in the \code{hash} argument.
|
||||
|
||||
To delete \emph{all} cached tokens, use \code{clean_token_directory}.
|
||||
}
|
||||
|
||||
\section{Value}{
|
||||
|
||||
For \code{get_azure_token}, an object of class \code{AzureToken} representing the AAD token. For \code{list_azure_tokens}, a list of such objects retrieved from disk.
|
||||
}
|
||||
|
||||
\examples{
|
||||
\dontrun{
|
||||
|
||||
# authenticate with Azure Resource Manager:
|
||||
# no user credentials are supplied, so this will use the authorization_code
|
||||
# method if httpuv is installed, and device_code if not
|
||||
arm_token <- get_azure_token(
|
||||
resource="https://management.azure.com/",
|
||||
tenant="myaadtenant.onmicrosoft.com",
|
||||
app="app_id")
|
||||
|
||||
# you can force a specific authentication method with the auth_type argument
|
||||
arm_token <- get_azure_token(
|
||||
resource="https://management.azure.com/",
|
||||
tenant="myaadtenant.onmicrosoft.com",
|
||||
app="app_id",
|
||||
auth_type="device_code")
|
||||
|
||||
# to use the client_credentials method, supply the app secret as the password
|
||||
arm_token <- get_azure_token(
|
||||
resource="https://management.azure.com/",
|
||||
tenant="myaadtenant.onmicrosoft.com",
|
||||
app="app_id",
|
||||
password="app_secret")
|
||||
|
||||
# authenticate with Azure storage
|
||||
storage_token <- get_azure_token(
|
||||
resource="https://storage.azure.com/",
|
||||
tenant="myaadtenant.onmicrosoft.com",
|
||||
app="app_id")
|
||||
|
||||
# authenticate to your resource with the resource_owner method: provide your username and password
|
||||
owner_token <- get_azure_token(
|
||||
resource="https://myresource/",
|
||||
tenant="myaadtenant",
|
||||
app="app_id",
|
||||
username="user",
|
||||
password="abcdefg")
|
||||
|
||||
# list saved tokens
|
||||
list_azure_tokens()
|
||||
|
||||
# delete a saved token from disk
|
||||
delete_azure_token(
|
||||
resource="https://myresource/",
|
||||
tenant="myaadtenant",
|
||||
app="app_id",
|
||||
username="user",
|
||||
password="abcdefg")
|
||||
|
||||
# delete a saved token by specifying its MD5 hash
|
||||
delete_azure_token(hash="7ea491716e5b10a77a673106f3f53bfd")
|
||||
|
||||
}
|
||||
}
|
||||
\seealso{
|
||||
\link{AzureToken}, \link[httr:oauth2.0_token]{httr::oauth2.0_token}, \link[httr:Token]{httr::Token},
|
||||
|
||||
\href{https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-code}{OAuth authentication for Azure Active Directory},
|
||||
\href{https://www.oauth.com/oauth2-servers/device-flow/token-request/}{Device code flow on OAuth.com},
|
||||
\href{https://tools.ietf.org/html/rfc6749}{OAuth 2.0 RFC} for the gory details on how OAuth works
|
||||
}
|
63
man/guid.Rd
63
man/guid.Rd
|
@ -1,63 +0,0 @@
|
|||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/normalize.R
|
||||
\name{normalize_tenant}
|
||||
\alias{normalize_tenant}
|
||||
\alias{normalize_guid}
|
||||
\alias{is_guid}
|
||||
\title{Normalize GUID and tenant values}
|
||||
\usage{
|
||||
normalize_tenant(tenant)
|
||||
|
||||
normalize_guid(x)
|
||||
|
||||
is_guid(x)
|
||||
}
|
||||
\arguments{
|
||||
\item{tenant}{For \code{normalize_tenant}, a string containing an Azure Active Directory tenant. This can be a name ("myaadtenant"), a fully qualified domain name ("myaadtenant.onmicrosoft.com" or "mycompanyname.com"), or a valid GUID.}
|
||||
|
||||
\item{x}{For \code{is_guid}, a character string; for \code{normalize_guid}, a string containing a \emph{validly formatted} GUID.}
|
||||
}
|
||||
\value{
|
||||
For \code{is_guid}, whether the argument is a validly formatted GUID.
|
||||
|
||||
For \code{normalize_guid}, the GUID in canonical format. If the argument is not recognised as a GUID, it throws an error.
|
||||
|
||||
For \code{normalize_tenant}, the normalized ID or name of the tenant.
|
||||
}
|
||||
\description{
|
||||
These functions are used by \code{get_azure_token} to recognise and properly format tenant and app IDs.
|
||||
}
|
||||
\details{
|
||||
A tenant can be identified either by a GUID, or its name, or a fully-qualified domain name (FQDN). The rules for normalizing a tenant are:
|
||||
\enumerate{
|
||||
\item If \code{tenant} is recognised as a valid GUID, return its canonically formatted value
|
||||
\item Otherwise, if it is a FQDN, return it
|
||||
\item Otherwise, if it is not the string "common", append ".onmicrosoft.com" to it
|
||||
\item Otherwise, return the value of \code{tenant}
|
||||
}
|
||||
|
||||
See the link below for GUID formats recognised by these functions.
|
||||
}
|
||||
\examples{
|
||||
|
||||
is_guid("72f988bf-86f1-41af-91ab-2d7cd011db47") # TRUE
|
||||
is_guid("{72f988bf-86f1-41af-91ab-2d7cd011db47}") # TRUE
|
||||
is_guid("72f988bf-86f1-41af-91ab-2d7cd011db47}") # FALSE (unmatched brace)
|
||||
is_guid("microsoft") # FALSE
|
||||
|
||||
# all of these return the same value
|
||||
normalize_guid("72f988bf-86f1-41af-91ab-2d7cd011db47")
|
||||
normalize_guid("{72f988bf-86f1-41af-91ab-2d7cd011db47}")
|
||||
normalize_guid("(72f988bf-86f1-41af-91ab-2d7cd011db47)")
|
||||
normalize_guid("72f988bf86f141af91ab2d7cd011db47")
|
||||
|
||||
normalize_tenant("microsoft") # returns 'microsoft.onmicrosoft.com'
|
||||
normalize_tenant("microsoft.com") # returns 'microsoft.com'
|
||||
normalize_tenant("72f988bf-86f1-41af-91ab-2d7cd011db47") # returns the GUID
|
||||
|
||||
}
|
||||
\seealso{
|
||||
\link{get_azure_token}
|
||||
|
||||
\href{https://docs.microsoft.com/en-us/dotnet/api/system.guid.parse]}{Parsing rules for GUIDs in .NET}. \code{is_guid} and \code{normalize_guid} recognise the "N", "D", "B" and "P" formats.
|
||||
}
|
|
@ -6,7 +6,6 @@
|
|||
\alias{is_resource_group}
|
||||
\alias{is_resource}
|
||||
\alias{is_template}
|
||||
\alias{is_azure_token}
|
||||
\title{Informational functions}
|
||||
\usage{
|
||||
is_azure_login(object)
|
||||
|
@ -18,8 +17,6 @@ is_resource_group(object)
|
|||
is_resource(object)
|
||||
|
||||
is_template(object)
|
||||
|
||||
is_azure_token(object)
|
||||
}
|
||||
\arguments{
|
||||
\item{object}{An R object.}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/reexport_AzureAuth.R
|
||||
\docType{import}
|
||||
\name{reexports}
|
||||
\alias{reexports}
|
||||
\alias{clean_token_directory}
|
||||
\alias{delete_azure_token}
|
||||
\alias{get_azure_token}
|
||||
\alias{is_azure_token}
|
||||
\alias{is_guid}
|
||||
\alias{list_azure_tokens}
|
||||
\alias{AzureR_dir}
|
||||
\title{Objects exported from other packages}
|
||||
\keyword{internal}
|
||||
\description{
|
||||
These objects are imported from other packages. Follow the links
|
||||
below to see their documentation.
|
||||
|
||||
\describe{
|
||||
\item{AzureAuth}{\code{\link[AzureAuth]{clean_token_directory}}, \code{\link[AzureAuth]{delete_azure_token}}, \code{\link[AzureAuth]{get_azure_token}}, \code{\link[AzureAuth]{is_azure_token}}, \code{\link[AzureAuth]{is_guid}}, \code{\link[AzureAuth]{list_azure_tokens}}, \code{\link[AzureAuth]{AzureR_dir}}}
|
||||
}}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
context("AzureToken")
|
||||
|
||||
test_that("normalize_tenant, normalize_guid work",
|
||||
{
|
||||
guid <- "abcdefab-1234-5678-9012-abcdefabcdef"
|
||||
expect_identical(normalize_guid(guid), guid)
|
||||
guid2 <- paste0("{", guid, "}")
|
||||
expect_identical(normalize_guid(guid2), guid)
|
||||
guid3 <- paste0("(", guid, ")")
|
||||
expect_identical(normalize_guid(guid3), guid)
|
||||
guid4 <- gsub("-", "", guid, fixed=TRUE)
|
||||
expect_identical(normalize_guid(guid4), guid)
|
||||
|
||||
# improperly formatted GUID will be treated as a name
|
||||
guid5 <- paste0("(", guid)
|
||||
expect_false(is_guid(guid5))
|
||||
expect_error(normalize_guid(guid5))
|
||||
expect_identical(normalize_tenant(guid5), paste0(guid5, ".onmicrosoft.com"))
|
||||
|
||||
expect_identical(normalize_tenant("common"), "common")
|
||||
expect_identical(normalize_tenant("mytenant"), "mytenant.onmicrosoft.com")
|
||||
expect_identical(normalize_tenant("mytenant.com"), "mytenant.com")
|
||||
# iterating normalize shouldn't change result
|
||||
expect_identical(normalize_tenant(normalize_tenant("mytenant")), "mytenant.onmicrosoft.com")
|
||||
})
|
||||
|
||||
|
||||
tenant <- Sys.getenv("AZ_TEST_TENANT_ID")
|
||||
app <- Sys.getenv("AZ_TEST_APP_ID")
|
||||
password <- Sys.getenv("AZ_TEST_PASSWORD")
|
||||
subscription <- Sys.getenv("AZ_TEST_SUBSCRIPTION")
|
||||
native_app <- Sys.getenv("AZ_TEST_NATIVE_APP_ID")
|
||||
|
||||
if(tenant == "" || app == "" || password == "" || subscription == "" || native_app == "")
|
||||
skip("Authentication tests skipped: ARM credentials not set")
|
||||
|
||||
if(system.file(package="httpuv") == "")
|
||||
skip("Authentication tests skipped: httpuv must be installed")
|
||||
|
||||
# not a perfect test: will fail to detect Linux DSVM issue
|
||||
if(!interactive())
|
||||
skip("Authentication tests skipped: must be an interactive session")
|
||||
|
||||
test_that("Authentication works",
|
||||
{
|
||||
suppressWarnings(file.remove(dir(AzureRMR:::AzureRMR_dir(), full.names=TRUE)))
|
||||
|
||||
res <- "https://management.azure.com/"
|
||||
|
||||
# obtain new tokens
|
||||
aut_tok <- get_azure_token(res, tenant, native_app, auth_type="authorization_code")
|
||||
expect_true(is_azure_token(aut_tok))
|
||||
expect_identical(aut_tok$hash(), "b29ef592fa435a4fd92672daf8726bae")
|
||||
|
||||
ccd_tok <- get_azure_token(res, tenant, app, password=password)
|
||||
expect_true(is_azure_token(ccd_tok))
|
||||
expect_identical(ccd_tok$hash(), "c75c266d9c578af29e24d3f22013ebf6")
|
||||
|
||||
dev_tok <- get_azure_token(res, tenant, native_app, auth_type="device_code")
|
||||
expect_true(is_azure_token(dev_tok))
|
||||
expect_identical(dev_tok$hash(), "37cbd9fec7c15b5a47edc1ea6f2f2747")
|
||||
|
||||
aut_expire <- as.numeric(aut_tok$credentials$expires_on)
|
||||
ccd_expire <- as.numeric(ccd_tok$credentials$expires_on)
|
||||
dev_expire <- as.numeric(dev_tok$credentials$expires_on)
|
||||
|
||||
Sys.sleep(5)
|
||||
|
||||
# refresh/reauthenticate
|
||||
aut_tok$refresh()
|
||||
ccd_tok$refresh()
|
||||
dev_tok$refresh()
|
||||
|
||||
expect_true(as.numeric(aut_tok$credentials$expires_on) > aut_expire)
|
||||
expect_true(as.numeric(ccd_tok$credentials$expires_on) > ccd_expire)
|
||||
expect_true(as.numeric(dev_tok$credentials$expires_on) > dev_expire)
|
||||
|
||||
# load cached tokens: should not get repeated login prompts/screens
|
||||
aut_tok2 <- get_azure_token(res, tenant, native_app, auth_type="authorization_code")
|
||||
expect_true(is_azure_token(aut_tok2))
|
||||
expect_identical(aut_tok2$hash(), "b29ef592fa435a4fd92672daf8726bae")
|
||||
|
||||
ccd_tok2 <- get_azure_token(res, tenant, app, password=password)
|
||||
expect_true(is_azure_token(ccd_tok2))
|
||||
expect_identical(ccd_tok2$hash(), "c75c266d9c578af29e24d3f22013ebf6")
|
||||
|
||||
dev_tok2 <- get_azure_token(res, tenant, native_app, auth_type="device_code")
|
||||
expect_true(is_azure_token(dev_tok2))
|
||||
expect_identical(dev_tok2$hash(), "37cbd9fec7c15b5a47edc1ea6f2f2747")
|
||||
|
||||
expect_null(delete_azure_token(res, tenant, native_app, auth_type="authorization_code", confirm=FALSE))
|
||||
expect_null(delete_azure_token(res, tenant, app, password=password, confirm=FALSE))
|
||||
expect_null(delete_azure_token(res, tenant, native_app, auth_type="device_code", confirm=FALSE))
|
||||
})
|
Загрузка…
Ссылка в новой задаче