Skip to content

Commit

Permalink
Implement API key security. (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonthegeek authored Oct 11, 2023
1 parent 8106cae commit f81f526
Show file tree
Hide file tree
Showing 25 changed files with 278 additions and 43 deletions.
5 changes: 3 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ Description: Automatically generate R package skeletons from 'application
(OAS)'. The skeletons implement best practices to streamline package
development.
License: MIT + file LICENSE
URL: https://jonthegeek.github.io/beekeeper/,
https://github.com/jonthegeek/beekeeper
URL: https://beekeeper.api2r.org, https://github.com/jonthegeek/beekeeper
BugReports: https://github.com/jonthegeek/beekeeper/issues
Imports:
cli,
Expand All @@ -21,10 +20,12 @@ Imports:
glue,
lubridate,
nectar,
purrr,
rapid,
rlang (>= 1.1.0),
rprojroot,
S7,
snakecase,
stbl,
styler,
testthat,
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ if (getRversion() < "4.3.0") importFrom("S7", "@")
importFrom(glue,glue)
importFrom(nectar,call_api)
importFrom(rapid,as_rapid)
importFrom(rlang,.data)
importFrom(testthat,test_that)
importFrom(usethis,use_build_ignore)
importFrom(usethis,use_package)
1 change: 1 addition & 0 deletions R/beekeeper-package.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#' @importFrom glue glue
#' @importFrom nectar call_api
#' @importFrom rapid as_rapid
#' @importFrom rlang .data
#' @importFrom testthat test_that
#' @importFrom usethis use_package
## usethis namespace: end
Expand Down
34 changes: 18 additions & 16 deletions R/generate.R
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ generate_pkg <- function(config_file = "_beekeeper.yml",
api_definition <- .read_api_definition(config_file, config$rapid_file)
.prepare_r()

touched_files <- .generate_call(
touched_files <- .generate_basics(
api_title = config$api_title,
api_abbr = config$api_abbr,
base_url = api_definition@servers@url,
pkg_agent = pkg_agent
pkg_agent = pkg_agent,
security_schemes = api_definition@components@security_schemes
)

return(invisible(touched_files))
Expand All @@ -34,24 +35,25 @@ generate_pkg <- function(config_file = "_beekeeper.yml",
)
}

.generate_call <- function(api_title,
api_abbr,
base_url,
pkg_agent) {
api_title <- .stabilize_chr_scalar_nonempty(api_title)
api_abbr <- .stabilize_chr_scalar_nonempty(api_abbr)
base_url <- .stabilize_chr_scalar_nonempty(base_url)
pkg_agent <- .stabilize_chr_scalar_nonempty(pkg_agent)
.generate_basics <- function(api_title,
api_abbr,
base_url,
pkg_agent,
security_schemes) {
security_data <- .generate_security(api_abbr, security_schemes)

data <- list(
api_title = .stabilize_chr_scalar_nonempty(api_title),
api_abbr = .stabilize_chr_scalar_nonempty(api_abbr),
base_url = .stabilize_chr_scalar_nonempty(base_url),
pkg_agent = .stabilize_chr_scalar_nonempty(pkg_agent)
)
data <- c(data, security_data)

touched_files <- c(
.bk_use_template(
template = "010-call.R",
data = list(
api_title = api_title,
api_abbr = api_abbr,
base_url = base_url,
pkg_agent = pkg_agent
)
data = data
),
.bk_use_template(
template = "test-010-call.R",
Expand Down
98 changes: 98 additions & 0 deletions R/security.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
.check_api_key <- function(security_schemes) {
api_key_scheme_idx <- purrr::map_lgl(
security_schemes@details,
function(x) {
inherits(x, "rapid::api_key_security_scheme")
}
)
if (any(api_key_scheme_idx)) {
return(
.extract_api_key_details(security_schemes, api_key_scheme_idx)
)
}
return(list())
}

.extract_api_key_details <- function(security_schemes, api_key_scheme_idx) {
purrr::pmap(
list(
security_schemes@name[api_key_scheme_idx],
security_schemes@details[api_key_scheme_idx],
security_schemes@description[api_key_scheme_idx]
),
function(security_scheme_name, details, description) {
if (is.na(description)) {
description <- "An API key provided by the API provider. This key is not clearly documented in the API description. Check the API documentation for details."
}
list(
name = snakecase::to_snake_case(security_scheme_name),
arg_name = stringr::str_remove(
snakecase::to_snake_case(details@parameter_name),
"^x_"
),
location = details@location,
parameter_name = details@parameter_name,
description = description
)
}
)
}

.generate_security <- function(api_abbr, security_schemes) {
security_data <- list()
api_key_data <- .check_api_key(security_schemes)
if (length(api_key_data)) {
security_data$api_schemes <- api_key_data
security_data$has_security <- TRUE
security_arg_names <- sort(
unique(purrr::map_chr(api_key_data, "arg_name"))
)
security_data$security_arg_names <- security_arg_names
.bk_use_template(
template = "020-security.R",
data = c(
security_data,
api_abbr = api_abbr
)
)
security_data$security_args <- glue::glue_collapse(
glue::glue(
"{security_arg_names} = {security_arg_names}"
),
sep = ",\n"
)

# For help.
security_arg_description <- rlang::set_names(
purrr::map_chr(api_key_data, "description"),
purrr::map_chr(api_key_data, "arg_name")
)
security_arg_description <- unname(
security_arg_description[security_arg_names]
)
security_data$security_arg_helps <- purrr::map2(
security_arg_description,
security_arg_names,
function(arg_description, arg_name) {
list(
name = arg_name,
description = arg_description
)
}
)

env_vars <- toupper(glue::glue(
"{api_abbr}_{security_arg_names}"
))
security_data$security_signature <- glue::glue_collapse(
c(
"",
glue::glue(
"{security_arg_names} = Sys.getenv(\"{env_vars}\")"
)
),
sep = ",\n"
)
}
return(security_data)
}
4 changes: 2 additions & 2 deletions README.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ knitr::opts_chunk$set(
)
```

# beekeeper <a href="https://jonthegeek.github.io/beekeeper/"><img src="man/figures/logo.svg" align="right" height="120" /></a>
# beekeeper <a href="https://beekeeper.api2r.org"><img src="man/figures/logo.svg" align="right" height="120" /></a>

<!-- badges: start -->
[![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental)
Expand Down Expand Up @@ -105,4 +105,4 @@ In addition to the pun, I hope to eventually include a "hive" of hex logos on th

## Code of Conduct

Please note that the beekeeper project is released with a [Contributor Code of Conduct](https://jonthegeek.github.io/beekeeper/CODE_OF_CONDUCT.html). By contributing to this project, you agree to abide by its terms.
Please note that the beekeeper project is released with a [Contributor Code of Conduct](https://beekeeper.api2r.org/CODE_OF_CONDUCT.html). By contributing to this project, you agree to abide by its terms.
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

<!-- README.md is generated from README.Rmd. Please edit that file -->

# beekeeper <a href="https://jonthegeek.github.io/beekeeper/"><img src="man/figures/logo.svg" align="right" height="120" /></a>
# beekeeper <a href="https://beekeeper.api2r.org"><img src="man/figures/logo.svg" align="right" height="120" /></a>

<!-- badges: start -->

Expand Down Expand Up @@ -155,6 +155,5 @@ been “tended” by {beekeeper}.
## Code of Conduct

Please note that the beekeeper project is released with a [Contributor
Code of
Conduct](https://jonthegeek.github.io/beekeeper/CODE_OF_CONDUCT.html).
By contributing to this project, you agree to abide by its terms.
Code of Conduct](https://beekeeper.api2r.org/CODE_OF_CONDUCT.html). By
contributing to this project, you agree to abide by its terms.
2 changes: 1 addition & 1 deletion _pkgdown.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
url: https://jonthegeek.github.io/beekeeper/
url: https://beekeeper.api2r.org
template:
bootstrap: 5
13 changes: 10 additions & 3 deletions inst/templates/010-call.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@
#'
#' Generate a request to an {{api_title}} endpoint.
#'
#' @inheritParams nectar::call_api
#' @inheritParams nectar::call_api{{#security_arg_helps}}
#' @param {{name}} {{{description}}}{{/security_arg_helps}}
#'
#' @return The response from the endpoint.
#' @export
{{api_abbr}}_call_api <- function(path,
method = NULL) {
query = NULL,
body = NULL,
method = NULL{{{security_signature}}}) {
nectar::call_api(
base_url = "{{base_url}}",
path = path,
query = query,
body = body,
method = method,
user_agent = "{{pkg_agent}}"
user_agent = "{{pkg_agent}}"{{#has_security}},
security_fn = {{api_abbr}}_security,
security_args = list({{security_args}}){{/has_security}}
)
}
29 changes: 29 additions & 0 deletions inst/templates/020-security.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# These functions were generated by the {beekeeper} package, based on
# components@security_schemes from the source API description. You may want to
# delete unused options. In addition, APIs often have additional security
# options that are not formally documented in the API description. For example,
# for any `location = query` `api_key` options, it might be possible to instead
# pass the same parameter in a header, possibly with a different name. Consult
# the text description of authentication in your API documentation.

{{api_abbr}}_security <- function(req, {{security_arg_names}}) {
{{#api_schemes}}
req <- {{api_abbr}}_security_{{name}}(req, {{arg_name}})
{{/api_schemes}}
return(req)
}

{{#api_schemes}}
{{#description}}
# {{description}}
{{/description}}
{{api_abbr}}_security_{{name}} <- function(req, {{arg_name}}) {
nectar::security_api_key(
req,
location = "{{location}}",
parameter_name = "{{parameter_name}}",
api_key = {{arg_name}}
)
}

{{/api_schemes}}
2 changes: 1 addition & 1 deletion man/beekeeper-package.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions tests/testthat/_fixtures/000-create_fixtures.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
apid_url <- "https://api.apis.guru/v2/specs/apis.guru/2.2.0/openapi.yaml"
api_abbr <- "guru"
rapid_write_path <- test_path(glue::glue("_fixtures/{api_abbr}_rapid.rds"))
config_path <- test_path(glue::glue("_fixtures/{api_abbr}_beekeeper.yml"))
rapid <- apid_url |>
url() |>
rapid::as_rapid()
use_beekeeper(
rapid,
api_abbr = api_abbr,
config_file = config_path,
rapid_file = rapid_write_path
)

apid_url <- "https://api.apis.guru/v2/specs/fec.gov/1.0/openapi.yaml"
api_abbr <- "fec"
rapid_write_path <- test_path(glue::glue("_fixtures/{api_abbr}_rapid.rds"))
config_path <- test_path(glue::glue("_fixtures/{api_abbr}_beekeeper.yml"))
rapid <- apid_url |>
url() |>
rapid::as_rapid()
use_beekeeper(
rapid,
api_abbr = api_abbr,
config_file = config_path,
rapid_file = rapid_write_path
)

apid_url <- "https://api.apis.guru/v2/specs/trello.com/1.0/openapi.yaml"
api_abbr <- "trello"
rapid_write_path <- test_path(glue::glue("_fixtures/{api_abbr}_rapid.rds"))
config_path <- test_path(glue::glue("_fixtures/{api_abbr}_beekeeper.yml"))
rapid <- apid_url |>
url() |>
rapid::as_rapid()
use_beekeeper(
rapid,
api_abbr = api_abbr,
config_file = config_path,
rapid_file = rapid_write_path
)

warning("Revert .Rbuildignore")
2 changes: 1 addition & 1 deletion tests/testthat/_fixtures/DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Description: Automatically generate R package skeletons from 'application
(OAS)'. The skeletons implement best practices to streamline package
development.
License: MIT + file LICENSE
URL: https://jonthegeek.github.io/beekeeper/,
URL: https://beekeeper.api2r.org,
https://github.com/jonthegeek/beekeeper
BugReports: https://github.com/jonthegeek/beekeeper/issues
Imports:
Expand Down
5 changes: 5 additions & 0 deletions tests/testthat/_fixtures/fec_beekeeper.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
api_title: OpenFEC
api_abbr: fec
api_version: '1.0'
rapid_file: fec_rapid.rds
updated_on: 2023-10-11 14:59:01.432621
Binary file added tests/testthat/_fixtures/fec_rapid.rds
Binary file not shown.
4 changes: 4 additions & 0 deletions tests/testthat/_fixtures/guru-010-call.R
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@
#' @return The response from the endpoint.
#' @export
guru_call_api <- function(path,
query = NULL,
body = NULL,
method = NULL) {
nectar::call_api(
base_url = "https://api.apis.guru/v2",
path = path,
query = query,
body = body,
method = method,
user_agent = "TESTPKG (https://example.com)"
)
Expand Down
4 changes: 2 additions & 2 deletions tests/testthat/_fixtures/guru_beekeeper.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
api_title: APIs.guru
api_abbr: guru
api_version: 2.2.0
rapid_file: _beekeeper_rapid.rds
updated_on: 2023-09-14 19:12:19.405029
rapid_file: guru_rapid.rds
updated_on: 2023-10-11 14:56:48.892847
Binary file modified tests/testthat/_fixtures/guru_rapid.rds
Binary file not shown.
Loading

0 comments on commit f81f526

Please sign in to comment.