-
Notifications
You must be signed in to change notification settings - Fork 57
Expand file tree
/
Copy pathgh_token.R
More file actions
211 lines (201 loc) · 6.67 KB
/
Copy pathgh_token.R
File metadata and controls
211 lines (201 loc) · 6.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
#' Return the local user's GitHub Personal Access Token (PAT)
#'
#' @description
#' If gh can find a personal access token (PAT) via `gh_token()`, it includes
#' the PAT in its requests. Some requests succeed without a PAT, but many
#' require a PAT to prove the request is authorized by a specific GitHub user. A
#' PAT also helps with rate limiting. If your gh use is more than casual, you
#' want a PAT.
#'
#' gh calls [gitcreds::gitcreds_get()] with the `api_url`, which checks session
#' environment variables (`GITHUB_PAT`, `GITHUB_TOKEN`)
#' and then the local Git credential store for a PAT
#' appropriate to the `api_url`. Therefore, if you have previously used a PAT
#' with, e.g., command line Git, gh may retrieve and re-use it. You can call
#' [gitcreds::gitcreds_get()] directly, yourself, if you want to see what is
#' found for a specific URL. If no matching PAT is found,
#' [gitcreds::gitcreds_get()] errors, whereas `gh_token()` does not and,
#' instead, returns `""`.
#'
#' See GitHub's documentation on [Creating a personal access
#' token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token),
#' or use `usethis::create_github_token()` for a guided experience, including
#' pre-selection of recommended scopes. Once you have a PAT, you can use
#' [gitcreds::gitcreds_set()] to add it to the Git credential store. From that
#' point on, gh (via [gitcreds::gitcreds_get()]) should be able to find it
#' without further effort on your part.
#'
#' # Token format validation
#'
#' gh warns if the PAT it retrieves does not match a known format.
#' Set `options(gh_validate_tokens = "off")` or the
#' `GH_VALIDATE_TOKENS=off` environment variable to avoid this warning.
#' The option takes precedence over the environment variable.
#'
#' Set `options(gh_validate_tokens = "error")` or the
#' `GH_VALIDATE_TOKENS=error` environment variable to make gh throw an
#' error for an unrecognized PAT format.
#'
#' @param api_url GitHub API URL. Defaults to the `GITHUB_API_URL` environment
#' variable, if set, and otherwise to <https://api.github.com>.
#'
#' @return A string of characters, if a PAT is found, or the empty
#' string, otherwise. For convenience, the return value has an S3 class in
#' order to ensure that simple printing strategies don't reveal the entire
#' PAT.
#'
#' @export
#'
#' @examples
#' \dontrun{
#' gh_token()
#'
#' format(gh_token())
#'
#' str(gh_token())
#' }
gh_token <- function(api_url = NULL) {
api_url <- api_url %||% default_api_url()
check_string(api_url)
host_url <- get_hosturl(api_url)
# Check for credentials supplied by Posit Connect.
if (is_installed("connectcreds")) {
if (connectcreds::has_viewer_token(host_url)) {
token <- connectcreds::connect_viewer_token(host_url)
return(gh_pat(token$access_token))
}
}
token <- tryCatch(
gitcreds::gitcreds_get(host_url),
error = function(e) NULL
)
gh_pat(token$password %||% "")
}
#' @export
#' @rdname gh_token
gh_token_exists <- function(api_url = NULL) {
tryCatch(nzchar(gh_token(api_url)), error = function(e) FALSE)
}
gh_auth <- function(token) {
if (isTRUE(token != "")) {
if (any(grepl("\\s", token))) {
warning("Token contains whitespace characters")
}
c("Authorization" = paste("token", trim_ws(token)))
} else {
character()
}
}
# gh_pat class: exists in order have a print method that hides info ----
new_gh_pat <- function(x) {
if (is.character(x) && length(x) == 1) {
structure(x, class = "gh_pat")
} else {
cli::cli_abort("A GitHub PAT must be a string")
}
}
# validates PAT only in a very narrow, technical, and local sense
validate_gh_pat <- function(x) {
if (!inherits(x, "gh_pat")) {
stop_input_type(x, "a <gh_pat> object")
}
mode <- get_validate_tokens_mode()
if (mode == "off") {
return(x)
}
if (
x == "" ||
# https://github.blog/changelog/2021-03-04-authentication-token-format-updates/
# Fine grained tokens start with "github_pat_".
# https://github.blog/changelog/2022-10-18-introducing-fine-grained-personal-access-tokens/
# GitHub App installation tokens start with "ghs_".
# https://github.blog/changelog/2026-04-24-notice-about-upcoming-new-format-for-github-app-installation-tokens/
grepl(
"^(gh[pousr]_[A-Za-z0-9_]{36,251}|github_pat_[A-Za-z0-9_]{36,244}|ghs_.+)$",
x
) ||
grepl("^[[:xdigit:]]{40}$", x)
) {
return(x)
}
url <- "https://gh.r-lib.org/articles/managing-personal-access-tokens.html"
msg <- c(
"Invalid GitHub PAT format",
"i" = "A GitHub PAT must have one of four forms:",
"*" = "40 hexadecimal digits (older PATs)",
"*" = "A 'ghp_' prefix followed by 36 to 251 more characters (newer
PATs)",
"*" = "A `ghs_` prefix followed by about 500 characters (GitHub App
installation tokens)",
"*" = "A 'github_pat_' prefix followed by 36 to 244 more characters
(fine-grained PATs)",
"i" = "Read more at {.url {url}}."
)
sil <- c(
"i" = "Set {.code options(gh_validate_tokens = \"off\")} or env var
{.envvar GH_VALIDATE_TOKENS=off} to silence this."
)
if (mode == "warn") {
cli::cli_warn(
c(msg, sil),
.frequency = "regularly",
.frequency_id = "gh_invalid_pat"
)
return(x)
}
cli::cli_abort(msg)
}
# Resolve token-validation mode: option > env var > "warn".
get_validate_tokens_mode <- function() {
opt <- getOption("gh_validate_tokens")
if (is.null(opt)) {
env <- Sys.getenv("GH_VALIDATE_TOKENS", unset = "")
mode <- if (nzchar(env)) env else "warn"
} else {
mode <- opt
}
if (!is.character(mode) || length(mode) != 1L || is.na(mode)) {
cli::cli_abort(c(
"Invalid token validation setting: must be a single string.",
"i" = "Got {.obj_type_friendly {mode}}."
))
}
mode <- tolower(mode)
if (!mode %in% c("off", "warn", "error")) {
cli::cli_abort(c(
"Invalid token validation mode: {.val {mode}}.",
"i" = "Must be one of {.val off}, {.val warn}, or {.val error}.",
"i" = "Configure via {.code options(gh_validate_tokens = ...)} or
env var {.envvar GH_VALIDATE_TOKENS}."
))
}
mode
}
gh_pat <- function(x) {
validate_gh_pat(new_gh_pat(x))
}
#' @export
format.gh_pat <- function(x, ...) {
if (x == "") {
"<no PAT>"
} else {
obfuscate(x)
}
}
#' @export
print.gh_pat <- function(x, ...) {
cat(format(x), sep = "\n")
invisible(x)
}
#' @export
str.gh_pat <- function(object, ...) {
cat(paste0("<gh_pat> ", format(object), "\n", collapse = ""))
invisible()
}
obfuscate <- function(x, first = 4, last = 4) {
paste0(
substr(x, start = 1, stop = first),
"...",
substr(x, start = nchar(x) - last + 1, stop = nchar(x))
)
}