Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 92 additions & 4 deletions core/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,54 @@ impl HTTPAuthentication for ForceHTTPBasicAuth {
}
}

/// Authentication policy that *always* includes a given header
#[derive(Debug, Clone)]
struct HeaderAuth {
pub header: String,
pub value: String,
}

impl HTTPAuthentication for HeaderAuth {
async fn request_with_authentication<F>(
&self,
request: RequestBuilder,
_renew_request: &F,
) -> Result<Response, reqwest_middleware::Error>
where
F: Fn(&ClientWithMiddleware) -> RequestBuilder + 'static,
{
request.header(&self.header, &self.value).send().await
}
}

/// Authentication policy that *always* includes a bearer token
#[derive(Debug, Clone)]
pub struct ForceBearerAuth(HeaderAuth);

impl ForceBearerAuth {
pub fn new<S: AsRef<str>>(token: S) -> ForceBearerAuth {
ForceBearerAuth(HeaderAuth {
header: "Authorization".to_string(),
value: format!("Bearer {}", token.as_ref()),
})
}
}

impl HTTPAuthentication for ForceBearerAuth {
async fn request_with_authentication<F>(
&self,
request: RequestBuilder,
renew_request: &F,
) -> Result<Response, reqwest_middleware::Error>
where
F: Fn(&ClientWithMiddleware) -> RequestBuilder + 'static,
{
self.0
.request_with_authentication(request, renew_request)
.await
}
}

/// First tries `Higher` priority authentication and then the
/// `Lower` priority one in case the first request results in
/// a response in the 4xx range.
Expand Down Expand Up @@ -307,6 +355,36 @@ impl<Restricted: HTTPAuthentication, Unrestricted: HTTPAuthentication> HTTPAuthe
}
}

#[derive(Debug, Clone)]
pub enum StandardInnerAuthentication {
HTTPBasicAuth(ForceHTTPBasicAuth),
BearerAuth(ForceBearerAuth),
}

impl HTTPAuthentication for StandardInnerAuthentication {
async fn request_with_authentication<F>(
&self,
request: RequestBuilder,
renew_request: &F,
) -> Result<Response, reqwest_middleware::Error>
where
F: Fn(&ClientWithMiddleware) -> RequestBuilder + 'static,
{
match self {
StandardInnerAuthentication::HTTPBasicAuth(inner) => {
inner
.request_with_authentication(request, renew_request)
.await
}
StandardInnerAuthentication::BearerAuth(inner) => {
inner
.request_with_authentication(request, renew_request)
.await
}
}
}
}

/// Standard HTTP authentication policy where a restricted set of domains/paths have
/// BasicAuth username/password pairs specified, but they are sent only in response to a
/// 4xx status code.
Expand All @@ -316,7 +394,7 @@ pub type StandardHTTPAuthentication = RestrictAuthentication<
Unauthenticated,
// ... but send username/password in response to 4xx.
// FIXME: Replace by a more general type as more authentication schemes are added
ForceHTTPBasicAuth,
StandardInnerAuthentication,
>,
// For all other domains use unauthenticated access.
Unauthenticated,
Expand All @@ -325,7 +403,7 @@ pub type StandardHTTPAuthentication = RestrictAuthentication<
/// Utility to simplify construction of `StandardHTTPAuthentication`
#[derive(Debug, Default, Clone)]
pub struct StandardHTTPAuthenticationBuilder {
partial: GlobMapBuilder<SequenceAuthentication<Unauthenticated, ForceHTTPBasicAuth>>,
partial: GlobMapBuilder<SequenceAuthentication<Unauthenticated, StandardInnerAuthentication>>,
}

impl StandardHTTPAuthenticationBuilder {
Expand All @@ -350,10 +428,20 @@ impl StandardHTTPAuthenticationBuilder {
globstr,
SequenceAuthentication {
higher: Unauthenticated {},
lower: ForceHTTPBasicAuth {
lower: StandardInnerAuthentication::HTTPBasicAuth(ForceHTTPBasicAuth {
username: username.as_ref().to_string(),
password: password.as_ref().to_string(),
},
}),
},
);
}

pub fn add_bearer_auth<S: AsRef<str>, T: AsRef<str>>(&mut self, globstr: S, token: T) {
self.partial.add(
globstr,
SequenceAuthentication {
higher: Unauthenticated {},
lower: StandardInnerAuthentication::BearerAuth(ForceBearerAuth::new(token)),
},
);
}
Expand Down
20 changes: 11 additions & 9 deletions core/src/project/reqwest_src.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,12 @@ impl<Policy> ReqwestSrcProjectAsync<Policy> {
// .header(reqwest::header::ACCEPT, "application/json")
// }

pub fn reqwest_src<P: AsRef<Utf8UnixPath>>(
&self,
path: P,
) -> reqwest_middleware::RequestBuilder {
self.client.get(self.src_url(path))
}
// pub fn reqwest_src<P: AsRef<Utf8UnixPath>>(
// &self,
// path: P,
// ) -> reqwest_middleware::RequestBuilder {
// self.client.get(self.src_url(path))
// }
}

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

let this_url = self.src_url(path);

let resp = self
.reqwest_src(&path)
.send()
.auth_policy
.with_authentication(&self.client, &move |client| client.get(this_url.clone()))
.await
.map_err(|e| ReqwestSrcError::Reqwest(self.src_url(&path).into(), e))?;
.map_err(|e| ReqwestSrcError::Reqwest(self.meta_url().into(), e))?;

if resp.status().is_success() {
Ok(resp
Expand Down
15 changes: 13 additions & 2 deletions docs/src/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ Project indices and remotely stored project KPARs (or sources) may require authe
to get authorised access. Sysand currently supports this for:

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

Support is planned for:

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

## Configuring

At the time of writing, authentication can only be configured through environment variables.
Providing credentials is done by setting environment variables following the pattern

Providing credentials for the Basic authentication scheme is done by setting environment variables following the pattern

```text
SYSAND_CRED_<X> = <PATTERN>
Expand Down Expand Up @@ -47,3 +49,12 @@ Credentials will *only* be sent to URLs matching the pattern, and even then only
unauthenticated response produces a status in the 4xx range. If multiple patterns match, they will
be tried in an arbitrary order, after the initial unauthenticated attempt, until one results in a
response not in the 4xx range.

Authentication by a (fixed) bearer token works similarly, using the pattern
```text
SYSAND_CRED_<X> = <PATTERN>
SYSAND_CRED_<X>_BEARER_TOKEN = <TOKEN>
```

With the above the Sysand client will send `Authorization: Bearer <TOKEN>`
in response to 4xx statuses when accessing URLs maching `<PATTERN>`.
52 changes: 41 additions & 11 deletions sysand/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,52 +168,82 @@ pub fn run_cli(args: cli::Args) -> Result<()> {
// FIXME: This is a temporary implementation to provide credentials until
// https://github.com/sensmetry/sysand/pull/157
// gets merged.
let mut basic_auth_patterns = HashMap::new();
let mut auth_patterns = HashMap::new();
let mut basic_auth_users = HashMap::new();
let mut basic_auth_passwords = HashMap::new();
let mut bearer_auth_tokens = HashMap::new();

for (key, value) in std::env::vars() {
if let Some(key_rest) = key.strip_prefix("SYSAND_CRED_") {
if let Some(key_name) = key_rest.strip_suffix("_BASIC_USER") {
basic_auth_users.insert(key_name.to_owned(), value);
} else if let Some(key_name) = key_rest.strip_suffix("_BASIC_PASS") {
basic_auth_passwords.insert(key_name.to_owned(), value);
} else if let Some(key_name) = key_rest.strip_suffix("_BEARER_TOKEN") {
bearer_auth_tokens.insert(key_name.to_owned(), value);
} else {
basic_auth_patterns.insert(key_rest.to_owned(), value);
auth_patterns.insert(key_rest.to_owned(), value);
}
}
}

let mut basic_auth_pattern_names = HashSet::new();
for x in [
&basic_auth_patterns,
&auth_patterns,
&basic_auth_users,
&basic_auth_passwords,
&bearer_auth_tokens,
] {
for k in x.keys() {
basic_auth_pattern_names.insert(k);
}
}

let mut basic_auths_builder: StandardHTTPAuthenticationBuilder =
let mut auths_builder: StandardHTTPAuthenticationBuilder =
StandardHTTPAuthenticationBuilder::new();
for k in basic_auth_pattern_names {
match (
basic_auth_patterns.get(k),
auth_patterns.get(k),
basic_auth_users.get(k),
basic_auth_passwords.get(k),
bearer_auth_tokens.get(k),
) {
(Some(pattern), Some(username), Some(password)) => {
basic_auths_builder.add_basic_auth(pattern, username, password);
}
_ => {
(Some(_), None, None, None) => {
anyhow::bail!(
"Please specify all of SYSAND_CRED_{k}, SYSAND_CRED_{k}_BASIC_USER, SYSAND_CRED_{k}_BASIC_PASS"
"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"
);
}
(Some(pattern), maybe_username, maybe_password, maybe_token) => {
let mut matched_schemes = 0;

match (maybe_username, maybe_password) {
(Some(username), Some(password)) => {
matched_schemes += 1;
auths_builder.add_basic_auth(pattern, username, password)
}
(None, None) => {}
(_, _) => {
anyhow::bail!(
"Please specify both (or neither) of SYSAND_CRED_{k}_BASIC_USER and SYSAND_CRED_{k}_BASIC_PASS"
);
}
}

if let Some(token) = maybe_token {
matched_schemes += 1;
auths_builder.add_bearer_auth(pattern, token);
}

if matched_schemes > 1 {
log::warn!("SYSAND_CRED_{k} has multiple authentication schemes!");
}
}
(None, _, _, _) => {
anyhow::bail!("please specify URL pattern SYSAND_CRED_{k} for credential");
}
}
}
let basic_auth_policy = Arc::new(basic_auths_builder.build()?);
let basic_auth_policy = Arc::new(auths_builder.build()?);

match args.command {
cli::Command::Init {
Expand Down
Loading
Loading