Skip to content

Commit ff2798b

Browse files
authored
Merge pull request #12465 from tomberek/tomberek.access-token-prefixing
Fine-grained access-tokens
2 parents 8384e41 + 1222438 commit ff2798b

File tree

5 files changed

+152
-13
lines changed

5 files changed

+152
-13
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#include <gtest/gtest.h>
2+
#include "fetchers.hh"
3+
#include "fetch-settings.hh"
4+
#include "json-utils.hh"
5+
#include <nlohmann/json.hpp>
6+
#include "tests/characterization.hh"
7+
8+
namespace nix::fetchers {
9+
10+
using nlohmann::json;
11+
12+
class AccessKeysTest : public ::testing::Test
13+
{
14+
protected:
15+
16+
public:
17+
void SetUp() override
18+
{
19+
experimentalFeatureSettings.experimentalFeatures.get().insert(Xp::Flakes);
20+
}
21+
void TearDown() override {}
22+
};
23+
24+
TEST_F(AccessKeysTest, singleOrgGitHub)
25+
{
26+
fetchers::Settings fetchSettings = fetchers::Settings{};
27+
fetchSettings.accessTokens.get().insert({"github.com/a", "token"});
28+
auto i = Input::fromURL(fetchSettings, "github:a/b");
29+
30+
auto token = i.scheme->getAccessToken(fetchSettings, "github.com", "github.com/a/b");
31+
ASSERT_EQ(token, "token");
32+
}
33+
34+
TEST_F(AccessKeysTest, nonMatches)
35+
{
36+
fetchers::Settings fetchSettings = fetchers::Settings{};
37+
fetchSettings.accessTokens.get().insert({"github.com", "token"});
38+
auto i = Input::fromURL(fetchSettings, "gitlab:github.com/evil");
39+
40+
auto token = i.scheme->getAccessToken(fetchSettings, "gitlab.com", "gitlab.com/github.com/evil");
41+
ASSERT_EQ(token, std::nullopt);
42+
}
43+
44+
TEST_F(AccessKeysTest, noPartialMatches)
45+
{
46+
fetchers::Settings fetchSettings = fetchers::Settings{};
47+
fetchSettings.accessTokens.get().insert({"github.com/partial", "token"});
48+
auto i = Input::fromURL(fetchSettings, "github:partial-match/repo");
49+
50+
auto token = i.scheme->getAccessToken(fetchSettings, "github.com", "github.com/partial-match");
51+
ASSERT_EQ(token, std::nullopt);
52+
}
53+
54+
TEST_F(AccessKeysTest, repoGitHub)
55+
{
56+
fetchers::Settings fetchSettings = fetchers::Settings{};
57+
fetchSettings.accessTokens.get().insert({"github.com", "token"});
58+
fetchSettings.accessTokens.get().insert({"github.com/a/b", "another_token"});
59+
fetchSettings.accessTokens.get().insert({"github.com/a/c", "yet_another_token"});
60+
auto i = Input::fromURL(fetchSettings, "github:a/a");
61+
62+
auto token = i.scheme->getAccessToken(fetchSettings, "github.com", "github.com/a/a");
63+
ASSERT_EQ(token, "token");
64+
65+
token = i.scheme->getAccessToken(fetchSettings, "github.com", "github.com/a/b");
66+
ASSERT_EQ(token, "another_token");
67+
68+
token = i.scheme->getAccessToken(fetchSettings, "github.com", "github.com/a/c");
69+
ASSERT_EQ(token, "yet_another_token");
70+
}
71+
72+
TEST_F(AccessKeysTest, multipleGitLab)
73+
{
74+
fetchers::Settings fetchSettings = fetchers::Settings{};
75+
fetchSettings.accessTokens.get().insert({"gitlab.com", "token"});
76+
fetchSettings.accessTokens.get().insert({"gitlab.com/a/b", "another_token"});
77+
auto i = Input::fromURL(fetchSettings, "gitlab:a/b");
78+
79+
auto token = i.scheme->getAccessToken(fetchSettings, "gitlab.com", "gitlab.com/a/b");
80+
ASSERT_EQ(token, "another_token");
81+
82+
token = i.scheme->getAccessToken(fetchSettings, "gitlab.com", "gitlab.com/a/c");
83+
ASSERT_EQ(token, "token");
84+
}
85+
86+
TEST_F(AccessKeysTest, multipleSourceHut)
87+
{
88+
fetchers::Settings fetchSettings = fetchers::Settings{};
89+
fetchSettings.accessTokens.get().insert({"git.sr.ht", "token"});
90+
fetchSettings.accessTokens.get().insert({"git.sr.ht/~a/b", "another_token"});
91+
auto i = Input::fromURL(fetchSettings, "sourcehut:a/b");
92+
93+
auto token = i.scheme->getAccessToken(fetchSettings, "git.sr.ht", "git.sr.ht/~a/b");
94+
ASSERT_EQ(token, "another_token");
95+
96+
token = i.scheme->getAccessToken(fetchSettings, "git.sr.ht", "git.sr.ht/~a/c");
97+
ASSERT_EQ(token, "token");
98+
}
99+
100+
}

src/libfetchers-tests/meson.build

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@ add_project_arguments(
4646
subdir('nix-meson-build-support/common')
4747

4848
sources = files(
49+
'access-tokens.cc',
50+
'git-utils.cc',
4951
'public-key.cc',
50-
'git-utils.cc'
5152
)
5253

5354
include_dirs = [include_directories('.')]

src/libfetchers/fetch-settings.hh

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ struct Settings : public Config
2323
Access tokens are specified as a string made up of
2424
space-separated `host=token` values. The specific token
2525
used is selected by matching the `host` portion against the
26-
"host" specification of the input. The actual use of the
27-
`token` value is determined by the type of resource being
28-
accessed:
26+
"host" specification of the input. The `host` portion may
27+
contain a path element which will match against the prefix
28+
URL for the input. (eg: `github.com/org=token`). The actual use
29+
of the `token` value is determined by the type of resource
30+
being accessed:
2931
3032
* Github: the token value is the OAUTH-TOKEN string obtained
3133
as the Personal Access Token from the Github server (see

src/libfetchers/fetchers.hh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,9 @@ struct InputScheme
264264

265265
virtual std::optional<std::string> isRelative(const Input & input) const
266266
{ return std::nullopt; }
267+
268+
virtual std::optional<std::string> getAccessToken(const fetchers::Settings & settings, const std::string & host, const std::string & url) const
269+
{ return {};}
267270
};
268271

269272
void registerInputScheme(std::shared_ptr<InputScheme> && fetcher);

src/libfetchers/github.cc

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -172,20 +172,53 @@ struct GitArchiveInputScheme : InputScheme
172172
return input;
173173
}
174174

175-
std::optional<std::string> getAccessToken(const fetchers::Settings & settings, const std::string & host) const
175+
// Search for the longest possible match starting from the begining and ending at either the end or a path segment.
176+
std::optional<std::string> getAccessToken(const fetchers::Settings & settings, const std::string & host, const std::string & url) const override
176177
{
177178
auto tokens = settings.accessTokens.get();
179+
std::string answer;
180+
size_t answer_match_len = 0;
181+
if(! url.empty()) {
182+
for (auto & token : tokens) {
183+
auto first = url.find(token.first);
184+
if (
185+
first != std::string::npos
186+
&& token.first.length() > answer_match_len
187+
&& first == 0
188+
&& url.substr(0,token.first.length()) == token.first
189+
&& (url.length() == token.first.length() || url[token.first.length()] == '/')
190+
)
191+
{
192+
answer = token.second;
193+
answer_match_len = token.first.length();
194+
}
195+
}
196+
if (!answer.empty())
197+
return answer;
198+
}
178199
if (auto token = get(tokens, host))
179200
return *token;
180201
return {};
181202
}
182203

183204
Headers makeHeadersWithAuthTokens(
184205
const fetchers::Settings & settings,
185-
const std::string & host) const
206+
const std::string & host,
207+
const Input & input) const
208+
{
209+
auto owner = getStrAttr(input.attrs, "owner");
210+
auto repo = getStrAttr(input.attrs, "repo");
211+
auto hostAndPath = fmt( "%s/%s/%s", host, owner, repo);
212+
return makeHeadersWithAuthTokens(settings, host, hostAndPath);
213+
}
214+
215+
Headers makeHeadersWithAuthTokens(
216+
const fetchers::Settings & settings,
217+
const std::string & host,
218+
const std::string & hostAndPath) const
186219
{
187220
Headers headers;
188-
auto accessToken = getAccessToken(settings, host);
221+
auto accessToken = getAccessToken(settings, host, hostAndPath);
189222
if (accessToken) {
190223
auto hdr = accessHeaderFromToken(*accessToken);
191224
if (hdr)
@@ -366,7 +399,7 @@ struct GitHubInputScheme : GitArchiveInputScheme
366399
: "https://%s/api/v3/repos/%s/%s/commits/%s",
367400
host, getOwner(input), getRepo(input), *input.getRef());
368401

369-
Headers headers = makeHeadersWithAuthTokens(*input.settings, host);
402+
Headers headers = makeHeadersWithAuthTokens(*input.settings, host, input);
370403

371404
auto json = nlohmann::json::parse(
372405
readFile(
@@ -383,7 +416,7 @@ struct GitHubInputScheme : GitArchiveInputScheme
383416
{
384417
auto host = getHost(input);
385418

386-
Headers headers = makeHeadersWithAuthTokens(*input.settings, host);
419+
Headers headers = makeHeadersWithAuthTokens(*input.settings, host, input);
387420

388421
// If we have no auth headers then we default to the public archive
389422
// urls so we do not run into rate limits.
@@ -440,7 +473,7 @@ struct GitLabInputScheme : GitArchiveInputScheme
440473
auto url = fmt("https://%s/api/v4/projects/%s%%2F%s/repository/commits?ref_name=%s",
441474
host, getStrAttr(input.attrs, "owner"), getStrAttr(input.attrs, "repo"), *input.getRef());
442475

443-
Headers headers = makeHeadersWithAuthTokens(*input.settings, host);
476+
Headers headers = makeHeadersWithAuthTokens(*input.settings, host, input);
444477

445478
auto json = nlohmann::json::parse(
446479
readFile(
@@ -470,7 +503,7 @@ struct GitLabInputScheme : GitArchiveInputScheme
470503
host, getStrAttr(input.attrs, "owner"), getStrAttr(input.attrs, "repo"),
471504
input.getRev()->to_string(HashFormat::Base16, false));
472505

473-
Headers headers = makeHeadersWithAuthTokens(*input.settings, host);
506+
Headers headers = makeHeadersWithAuthTokens(*input.settings, host, input);
474507
return DownloadUrl { url, headers };
475508
}
476509

@@ -510,7 +543,7 @@ struct SourceHutInputScheme : GitArchiveInputScheme
510543
auto base_url = fmt("https://%s/%s/%s",
511544
host, getStrAttr(input.attrs, "owner"), getStrAttr(input.attrs, "repo"));
512545

513-
Headers headers = makeHeadersWithAuthTokens(*input.settings, host);
546+
Headers headers = makeHeadersWithAuthTokens(*input.settings, host, input);
514547

515548
std::string refUri;
516549
if (ref == "HEAD") {
@@ -557,7 +590,7 @@ struct SourceHutInputScheme : GitArchiveInputScheme
557590
host, getStrAttr(input.attrs, "owner"), getStrAttr(input.attrs, "repo"),
558591
input.getRev()->to_string(HashFormat::Base16, false));
559592

560-
Headers headers = makeHeadersWithAuthTokens(*input.settings, host);
593+
Headers headers = makeHeadersWithAuthTokens(*input.settings, host, input);
561594
return DownloadUrl { url, headers };
562595
}
563596

0 commit comments

Comments
 (0)