Skip to content

Commit d9eb645

Browse files
feat(cli): rudimentary support for bearer token authentication (#169)
Extends #165 with bearer token authentication. Configured by environment variables ``` SYSAND_CRED_<X> = <PATTERN> SYSAND_CRED_<X>_BEARER_TOKEN = <TOKEN> ``` --------- Signed-off-by: Tilo Wiklund <tilo.wiklund@sensmetry.com>
1 parent df5df0a commit d9eb645

5 files changed

Lines changed: 393 additions & 26 deletions

File tree

core/src/auth.rs

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,54 @@ impl HTTPAuthentication for ForceHTTPBasicAuth {
7272
}
7373
}
7474

75+
/// Authentication policy that *always* includes a given header
76+
#[derive(Debug, Clone)]
77+
struct HeaderAuth {
78+
pub header: String,
79+
pub value: String,
80+
}
81+
82+
impl HTTPAuthentication for HeaderAuth {
83+
async fn request_with_authentication<F>(
84+
&self,
85+
request: RequestBuilder,
86+
_renew_request: &F,
87+
) -> Result<Response, reqwest_middleware::Error>
88+
where
89+
F: Fn(&ClientWithMiddleware) -> RequestBuilder + 'static,
90+
{
91+
request.header(&self.header, &self.value).send().await
92+
}
93+
}
94+
95+
/// Authentication policy that *always* includes a bearer token
96+
#[derive(Debug, Clone)]
97+
pub struct ForceBearerAuth(HeaderAuth);
98+
99+
impl ForceBearerAuth {
100+
pub fn new<S: AsRef<str>>(token: S) -> ForceBearerAuth {
101+
ForceBearerAuth(HeaderAuth {
102+
header: "Authorization".to_string(),
103+
value: format!("Bearer {}", token.as_ref()),
104+
})
105+
}
106+
}
107+
108+
impl HTTPAuthentication for ForceBearerAuth {
109+
async fn request_with_authentication<F>(
110+
&self,
111+
request: RequestBuilder,
112+
renew_request: &F,
113+
) -> Result<Response, reqwest_middleware::Error>
114+
where
115+
F: Fn(&ClientWithMiddleware) -> RequestBuilder + 'static,
116+
{
117+
self.0
118+
.request_with_authentication(request, renew_request)
119+
.await
120+
}
121+
}
122+
75123
/// First tries `Higher` priority authentication and then the
76124
/// `Lower` priority one in case the first request results in
77125
/// a response in the 4xx range.
@@ -307,6 +355,36 @@ impl<Restricted: HTTPAuthentication, Unrestricted: HTTPAuthentication> HTTPAuthe
307355
}
308356
}
309357

358+
#[derive(Debug, Clone)]
359+
pub enum StandardInnerAuthentication {
360+
HTTPBasicAuth(ForceHTTPBasicAuth),
361+
BearerAuth(ForceBearerAuth),
362+
}
363+
364+
impl HTTPAuthentication for StandardInnerAuthentication {
365+
async fn request_with_authentication<F>(
366+
&self,
367+
request: RequestBuilder,
368+
renew_request: &F,
369+
) -> Result<Response, reqwest_middleware::Error>
370+
where
371+
F: Fn(&ClientWithMiddleware) -> RequestBuilder + 'static,
372+
{
373+
match self {
374+
StandardInnerAuthentication::HTTPBasicAuth(inner) => {
375+
inner
376+
.request_with_authentication(request, renew_request)
377+
.await
378+
}
379+
StandardInnerAuthentication::BearerAuth(inner) => {
380+
inner
381+
.request_with_authentication(request, renew_request)
382+
.await
383+
}
384+
}
385+
}
386+
}
387+
310388
/// Standard HTTP authentication policy where a restricted set of domains/paths have
311389
/// BasicAuth username/password pairs specified, but they are sent only in response to a
312390
/// 4xx status code.
@@ -316,7 +394,7 @@ pub type StandardHTTPAuthentication = RestrictAuthentication<
316394
Unauthenticated,
317395
// ... but send username/password in response to 4xx.
318396
// FIXME: Replace by a more general type as more authentication schemes are added
319-
ForceHTTPBasicAuth,
397+
StandardInnerAuthentication,
320398
>,
321399
// For all other domains use unauthenticated access.
322400
Unauthenticated,
@@ -325,7 +403,7 @@ pub type StandardHTTPAuthentication = RestrictAuthentication<
325403
/// Utility to simplify construction of `StandardHTTPAuthentication`
326404
#[derive(Debug, Default, Clone)]
327405
pub struct StandardHTTPAuthenticationBuilder {
328-
partial: GlobMapBuilder<SequenceAuthentication<Unauthenticated, ForceHTTPBasicAuth>>,
406+
partial: GlobMapBuilder<SequenceAuthentication<Unauthenticated, StandardInnerAuthentication>>,
329407
}
330408

331409
impl StandardHTTPAuthenticationBuilder {
@@ -350,10 +428,20 @@ impl StandardHTTPAuthenticationBuilder {
350428
globstr,
351429
SequenceAuthentication {
352430
higher: Unauthenticated {},
353-
lower: ForceHTTPBasicAuth {
431+
lower: StandardInnerAuthentication::HTTPBasicAuth(ForceHTTPBasicAuth {
354432
username: username.as_ref().to_string(),
355433
password: password.as_ref().to_string(),
356-
},
434+
}),
435+
},
436+
);
437+
}
438+
439+
pub fn add_bearer_auth<S: AsRef<str>, T: AsRef<str>>(&mut self, globstr: S, token: T) {
440+
self.partial.add(
441+
globstr,
442+
SequenceAuthentication {
443+
higher: Unauthenticated {},
444+
lower: StandardInnerAuthentication::BearerAuth(ForceBearerAuth::new(token)),
357445
},
358446
);
359447
}

core/src/project/reqwest_src.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,12 @@ impl<Policy> ReqwestSrcProjectAsync<Policy> {
7676
// .header(reqwest::header::ACCEPT, "application/json")
7777
// }
7878

79-
pub fn reqwest_src<P: AsRef<Utf8UnixPath>>(
80-
&self,
81-
path: P,
82-
) -> reqwest_middleware::RequestBuilder {
83-
self.client.get(self.src_url(path))
84-
}
79+
// pub fn reqwest_src<P: AsRef<Utf8UnixPath>>(
80+
// &self,
81+
// path: P,
82+
// ) -> reqwest_middleware::RequestBuilder {
83+
// self.client.get(self.src_url(path))
84+
// }
8585
}
8686

8787
#[derive(Error, Debug)]
@@ -164,11 +164,13 @@ impl<Policy: HTTPAuthentication> ProjectReadAsync for ReqwestSrcProjectAsync<Pol
164164
) -> Result<Self::SourceReader<'_>, Self::Error> {
165165
use futures::StreamExt as _;
166166

167+
let this_url = self.src_url(path);
168+
167169
let resp = self
168-
.reqwest_src(&path)
169-
.send()
170+
.auth_policy
171+
.with_authentication(&self.client, &move |client| client.get(this_url.clone()))
170172
.await
171-
.map_err(|e| ReqwestSrcError::Reqwest(self.src_url(&path).into(), e))?;
173+
.map_err(|e| ReqwestSrcError::Reqwest(self.meta_url().into(), e))?;
172174

173175
if resp.status().is_success() {
174176
Ok(resp

docs/src/authentication.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@ Project indices and remotely stored project KPARs (or sources) may require authe
44
to get authorised access. Sysand currently supports this for:
55

66
- HTTP(S) using the [basic access authentication scheme](https://en.wikipedia.org/wiki/Basic_access_authentication)
7+
- HTTP(S) using (fixed) bearer tokens (used by, for example, private GitLab pages)
78

89
Support is planned for:
910

10-
- HTTP(S) with digest access, (fixed) bearer token, and OAuth2 device authentication
11+
- HTTP(S) with digest access and OAuth2 device authentication
1112
- Git with private-key and basic access authentication
1213

1314
## Configuring
1415

1516
At the time of writing, authentication can only be configured through environment variables.
16-
Providing credentials is done by setting environment variables following the pattern
17+
18+
Providing credentials for the Basic authentication scheme is done by setting environment variables following the pattern
1719

1820
```text
1921
SYSAND_CRED_<X> = <PATTERN>
@@ -47,3 +49,12 @@ Credentials will *only* be sent to URLs matching the pattern, and even then only
4749
unauthenticated response produces a status in the 4xx range. If multiple patterns match, they will
4850
be tried in an arbitrary order, after the initial unauthenticated attempt, until one results in a
4951
response not in the 4xx range.
52+
53+
Authentication by a (fixed) bearer token works similarly, using the pattern
54+
```text
55+
SYSAND_CRED_<X> = <PATTERN>
56+
SYSAND_CRED_<X>_BEARER_TOKEN = <TOKEN>
57+
```
58+
59+
With the above the Sysand client will send `Authorization: Bearer <TOKEN>`
60+
in response to 4xx statuses when accessing URLs maching `<PATTERN>`.

sysand/src/lib.rs

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -168,52 +168,82 @@ pub fn run_cli(args: cli::Args) -> Result<()> {
168168
// FIXME: This is a temporary implementation to provide credentials until
169169
// https://github.com/sensmetry/sysand/pull/157
170170
// gets merged.
171-
let mut basic_auth_patterns = HashMap::new();
171+
let mut auth_patterns = HashMap::new();
172172
let mut basic_auth_users = HashMap::new();
173173
let mut basic_auth_passwords = HashMap::new();
174+
let mut bearer_auth_tokens = HashMap::new();
174175

175176
for (key, value) in std::env::vars() {
176177
if let Some(key_rest) = key.strip_prefix("SYSAND_CRED_") {
177178
if let Some(key_name) = key_rest.strip_suffix("_BASIC_USER") {
178179
basic_auth_users.insert(key_name.to_owned(), value);
179180
} else if let Some(key_name) = key_rest.strip_suffix("_BASIC_PASS") {
180181
basic_auth_passwords.insert(key_name.to_owned(), value);
182+
} else if let Some(key_name) = key_rest.strip_suffix("_BEARER_TOKEN") {
183+
bearer_auth_tokens.insert(key_name.to_owned(), value);
181184
} else {
182-
basic_auth_patterns.insert(key_rest.to_owned(), value);
185+
auth_patterns.insert(key_rest.to_owned(), value);
183186
}
184187
}
185188
}
186189

187190
let mut basic_auth_pattern_names = HashSet::new();
188191
for x in [
189-
&basic_auth_patterns,
192+
&auth_patterns,
190193
&basic_auth_users,
191194
&basic_auth_passwords,
195+
&bearer_auth_tokens,
192196
] {
193197
for k in x.keys() {
194198
basic_auth_pattern_names.insert(k);
195199
}
196200
}
197201

198-
let mut basic_auths_builder: StandardHTTPAuthenticationBuilder =
202+
let mut auths_builder: StandardHTTPAuthenticationBuilder =
199203
StandardHTTPAuthenticationBuilder::new();
200204
for k in basic_auth_pattern_names {
201205
match (
202-
basic_auth_patterns.get(k),
206+
auth_patterns.get(k),
203207
basic_auth_users.get(k),
204208
basic_auth_passwords.get(k),
209+
bearer_auth_tokens.get(k),
205210
) {
206-
(Some(pattern), Some(username), Some(password)) => {
207-
basic_auths_builder.add_basic_auth(pattern, username, password);
208-
}
209-
_ => {
211+
(Some(_), None, None, None) => {
210212
anyhow::bail!(
211-
"Please specify all of SYSAND_CRED_{k}, SYSAND_CRED_{k}_BASIC_USER, SYSAND_CRED_{k}_BASIC_PASS"
213+
"SYSAND_CRED_{k} has no matching authentication scheme, please specify SYSAND_CRED_{k}_BASIC_USER/SYSAND_CRED_{k}_BASIC_PASS or SYSAND_CRED_{k}_BEARER_TOKEN"
212214
);
213215
}
216+
(Some(pattern), maybe_username, maybe_password, maybe_token) => {
217+
let mut matched_schemes = 0;
218+
219+
match (maybe_username, maybe_password) {
220+
(Some(username), Some(password)) => {
221+
matched_schemes += 1;
222+
auths_builder.add_basic_auth(pattern, username, password)
223+
}
224+
(None, None) => {}
225+
(_, _) => {
226+
anyhow::bail!(
227+
"Please specify both (or neither) of SYSAND_CRED_{k}_BASIC_USER and SYSAND_CRED_{k}_BASIC_PASS"
228+
);
229+
}
230+
}
231+
232+
if let Some(token) = maybe_token {
233+
matched_schemes += 1;
234+
auths_builder.add_bearer_auth(pattern, token);
235+
}
236+
237+
if matched_schemes > 1 {
238+
log::warn!("SYSAND_CRED_{k} has multiple authentication schemes!");
239+
}
240+
}
241+
(None, _, _, _) => {
242+
anyhow::bail!("please specify URL pattern SYSAND_CRED_{k} for credential");
243+
}
214244
}
215245
}
216-
let basic_auth_policy = Arc::new(basic_auths_builder.build()?);
246+
let basic_auth_policy = Arc::new(auths_builder.build()?);
217247

218248
match args.command {
219249
cli::Command::Init {

0 commit comments

Comments
 (0)