File: list-creds.R

package info (click to toggle)
r-cran-gitcreds 0.1.2-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 448 kB
  • sloc: sh: 13; makefile: 2
file content (274 lines) | stat: -rw-r--r-- 9,959 bytes parent folder | download | duplicates (2)
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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274

#' List all credentials stored by a git credential helper
#'
#' This function is meant to be used interactively, to help you when
#' configuring credential helpers. It is especially useful if you have
#' multiple accounts on a host.
#'
#' Note that this function does not use the credential helper itself,
#' so it does not have to be installed. But it may also give false
#' results, so interpret the results with caution, and also use the tool
#' provided by your OS, to look at the credentials: 'Keychain Access'
#' on macOS and 'Credential Manager' on Windows.
#'
#' Only a small number of credential helpers are supported currently.
#' Here is a brief description of each.
#'
#' ## `osxkeychain` on macOS
#'
#' This is the default credential helper on macOS.
#'
#' It has some peculiarities:
#' * If you don't specify a username in the URL, then it will return the
#' _oldest_ credentials that match the specified host name, with an
#' arbitrary user name.
#' * If the user name is specified in the URL, then it is used to look up
#' the credentials.
#'
#' To change or delete the listed credentials, see the oskeyring package
#' or the 'Keychain Access' macOS app.
#'
#' ## `manager`, on Windows
#'
#' This is Git Credential Manager for Windows, see
#' https://github.com/microsoft/Git-Credential-Manager-for-Windows
#'
#' It is currently the default helper on Windows, included in the git
#' installer.
#'
#' It has some oddities, especially with multiple GitHub users:
#' * The `github` authority (which is used by default for `github.com`
#'   URLs) cannot handle multiple users. It always sets the `target_name`
#'   of the Windows credential to `git:<URL>` where `<URL>` does not
#'   contain the user name. Since `target_name` is a primary key, it is
#'   not possible to add multiple GitHub users with the default
#'   configuration.
#' * To support multiple users, switch to the `Basic` authority, e.g. by
#'   setting the `GCM_AUTHORITY` env var to `Basic`. Then the user name
#'   will be included in `target_name`, and everything works fine.
#' * For this helper `gitcreds_list()` lists all records with a matching
#'   host name.
#'
#' ## `manager-core` on Windows
#'
#' This is Git Credential Manager Core, see
#' https://github.com/microsoft/Git-Credential-Manager-Core
#'
#' On Windows it behaves almost the same way as `manager`, with some
#' differences:
#' * Instead of _authorities_, it has providers. `github.com` URLs use the
#'   `github` provider by default. For better support for multiple GitHub
#'   accounts, switch to the `generic` provider by setting the
#'   `GCM_PROVIDER` env var to `generic`.
#' * `gitcreds_list()` will list all credentials with a matching host,
#'   irrespectively of the user name in the input URL.
#'
#' ## `manager-core`, _before_ version 2.0.246-beta, on macOS
#'
#' This is Git Credential Manager Core, see
#' https://github.com/microsoft/Git-Credential-Manager-Core
#'
#' This helper has some peculiarities w.r.t. user names:
#' * If the "github" provider is used (which is the default for
#'   `github.com` URLs), then it completely ignores user names, even if
#'   they are explicitly specified in the query.
#' * For other providers, the user name (if specified) is saved in the
#'   Keychain item.
#' * For this helper, `gitcreds_list()` always lists all records that
#'   match the _host_, even if the user name does not match, because it
#'   is impossible to tell if the user name would be used in a proper
#'   git credential lookup.
#'
#' To change or delete the listed credentials, see the oskeyring package
#' or the 'Keychain Access' macOS app.
#'
#' ## `manager-core`, version 2.0.246-beta or newer, on macOS
#'
#' This is a newer version of Git Credential Manager Core, that supports
#' multiple users better:
#' * if a user name is provided, then it saves it in the credential store,
#'   and it uses this user name for looking up credentials, even for the
#'   `github` provider.
#' * `gitcreds_list()` always lists all records that match the host, even
#'   if the user name does not match.
#' * Credentials that were created by an older version of `manager-core`,
#'   with the `generic` provider, do not work with the newer version of
#'   `manager-core`, because the format of the Keychain item is different.
#'
#' @param credential_helper Credential helper to use. If this is `NULL`,
#'   then the configured credential helper is used. If multiple credential
#'   helpers are configured, then the first one is used, with a warning.
#' @param url URL to list credentials for. If `NULL` then the credentials
#'   are listed for all URLs. Note that for a host the results might be
#'   different if you specify or omit this argument. `gitcreds_list()`
#'   uses heuristics when the `url` is not specified. If is always best to
#'   specify the URL.
#' @param protocol Protocol to list credentials for. If `NULL` and `url`
#'   includes a protocol then that is used. Otherwise `"https"` is used.
#' @return A list of `oskeyring_macos_item` objects. See
#'   [oskeyring::macos_item()].

#' @export

gitcreds_list <- function(url = "https://github.com",
                          credential_helper = NULL,
                          protocol = NULL) {

  stopifnot(
    is.null(credential_helper) || gitcreds$is_string(credential_helper),
    is.null(url) || gitcreds$is_string(url),
    is.null(protocol) || gitcreds$is_string(protocol)
  )

  credential_helper <- credential_helper %||% gitcreds_list_helpers()
  if (length(credential_helper) == 0) {
    gitcreds$throw(gitcreds$new_error("gitcreds_no_helper"))
  }
  if (length(credential_helper) > 1) {
    gitcreds$throw(gitcreds$new_warning("gitcreds_multiple_helpers"))
    credential_helper <- credential_helper[[1]]
  }

  host <- NULL
  if (!is.null(url)) {
    purl <- gitcreds$parse_url(url)
    if (!is.na(purl$host)) host <- purl$host
    if (!is.na(purl$protocol)) protocol <- purl$protocol
  }
  protocol <- protocol %||% "https"

  switch(
    credential_helper,
    "osxkeychain" = gitcreds_list_osxkeychain(url, host, protocol),
    "manager" = gitcreds_list_manager(url, host, protocol),
    "manager-core" = gitcreds_list_manager_core(url, host, protocol),
    gitcreds$throw(gitcreds$new_error(
      "gitcreds_unknown_helper",
      credential_helper = credential_helper,
      message = sprintf(
        "Unknown credential helper: `%s`, cannot list credentials",
        credential_helper
      )
    ))
  )
}

#' This is how an item, added by git-credential-osxkeychain looks like:
#' * protocol is always present
#' * server is the same as label, but we ignore this when querying a
#'   specific host because the credential helper will ignore it as well
#' * security_domain is never present, similarly ignored for specific hosts. similar reasons
#' @noRd

gitcreds_list_osxkeychain <- function(url, host, protocol) {
  if (!requireNamespace("oskeyring", quietly=TRUE)) {
    stop("Listing `osxkeychain` credentials needs the `oskeyring` package")
  }

  attr <- list()
  if (!is.null(host)) {
    attr$server <- host
    attr$label <- host
  }

  attr$protocol <- protocol

  its <- oskeyring::macos_item_search(
    "internet_password",
    attributes = attr
  )

  Filter(is_osxkeychain_item, its)
}

is_osxkeychain_item <- function(it) {
  !is.null(it$attributes$label) &&
    !is.null(it$attributes$server) &&
    it$attributes$server == it$attributes$label &&
    is.null(it$attributes$security_domain)
}

gitcreds_list_manager_core <- function(url, host, protocol) {
  os <- gitcreds$get_os()
  if (os == "macos") {
    gitcreds_list_manager_core_macos(url, host, protocol)
  } else if (os == "windows") {
    gitcreds_list_manager_core_win(url, host, protocol)
  } else {
    stop("Unsupported OS for `manager-core`")
  }
}

gitcreds_list_manager_core_macos <- function(url, host, protocol) {
  if (!requireNamespace("oskeyring", quietly=TRUE)) {
    stop("Listing `manager-core` credentials needs the `oskeyring` package")
  }

  # We can't filter, need to list all of them, because the 'service'
  # might include the user name, if credential.provider is "github"
  # (or "auto" and the host is github.com).
  its <- oskeyring::macos_item_search("generic_password")

  its <- Filter(
    function(it) is_manager_core_macos_item(it, protocol, host),
    its
  )

  its
}

is_manager_core_macos_item <- function(it, protocol, host) {
  if (is.null(it$attributes$service)) return(FALSE)
  if (!grepl("^git:", it$attributes$service)) return(FALSE)
  if (is.null(host)) return(TRUE)
  iturl <- sub("^git:", "", it$attributes$service)
  piturl <- gitcreds$parse_url(iturl)
  !is.na(piturl$host) && piturl$host == host &&
    !is.na(piturl$protocol) && piturl$protocol == protocol
}

gitcreds_list_manager_core_win <- function(url, host, protocol) {
  if (!requireNamespace("oskeyring", quietly=TRUE)) {
    stop("Listing `manager-core` credentials needs the `oskeyring` package")
  }

  its <- oskeyring::windows_item_enumerate(filter = "git:*")

  its <- Filter(
    function(it) is_manager_core_win_item(it, protocol, host),
    its
  )

  its
}

is_manager_core_win_item <- function(it, protocol, host) {
  if (it$type != "generic") return(FALSE)
  if (!grepl("^git:", it$target_name)) return(FALSE)
  iturl <- sub("^git:", "", it$target_name)
  if (is.null(host)) return(TRUE)
  piturl <- gitcreds$parse_url(iturl)
  !is.na(piturl$host) && piturl$host == host &&
    !is.na(piturl$protocol) && piturl$protocol == protocol
}

gitcreds_list_manager <- function(url, host, protocol) {
  if (!requireNamespace("oskeyring", quietly=TRUE)) {
    stop("Listing `manager` credentials needs the `oskeyring` package")
  }

  its <- oskeyring::windows_item_enumerate(filter = "git:*")

  its <- Filter(
    function(it) is_manager_item(it, protocol, host),
    its
  )

  its
}

# this is the same, apparently

is_manager_item <- is_manager_core_win_item

`%||%` <- function(l, r) if (is.null(l)) r else l