mirror of
https://github.com/deuxfleurs-org/garage.git
synced 2026-09-06 03:59:15 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ed5721cd6 | |||
| 3daecd3465 | |||
| b14bcd15c2 | |||
| 9411e6c2ee | |||
| fbe76255d5 | |||
| 6f98c392b5 | |||
| b5006b7cce | |||
| b41e0f49bf |
@@ -0,0 +1,38 @@
|
||||
steps:
|
||||
helm:
|
||||
when:
|
||||
- event: pull_request
|
||||
branch: main-v2
|
||||
- event: push
|
||||
branch: main-v2
|
||||
image: alpine/helm:4.1.1
|
||||
environment:
|
||||
chart_dir: script/helm/garage
|
||||
chart_name: garage
|
||||
registry_host: codeberg.org
|
||||
registry_user: ${CI_REPO_OWNER}
|
||||
registry_path: ${CI_REPO}
|
||||
|
||||
# To create registry_password:
|
||||
# 1. Go to https://codeberg.org/user/settings/applications.
|
||||
# 2. Create a token with these settings:
|
||||
# - Token name: https://git.deuxfleurs.fr/3uzbcqje/garage/src/branch/main-v2/.woodpecker/helm.yaml
|
||||
# - Set package permission to "Read and write".
|
||||
# 3. Click "Generate token".
|
||||
# 4. Copy the token.
|
||||
# 5. Log into https://ci.codeberg.org.
|
||||
# 6. Find your repo, and add a secret with these settings:
|
||||
# - Name: registry_password
|
||||
# - Value: <the token you copied earlier>
|
||||
# 7. Click "Add secret".
|
||||
|
||||
registry_password:
|
||||
from_secret: registry_password
|
||||
|
||||
commands:
|
||||
- |
|
||||
if [[ "${CI_PIPELINE_EVENT}" == "push" && "${CI_COMMIT_BRANCH}" == "${CI_REPO_DEFAULT_BRANCH}" ]]; then
|
||||
./pipeline.sh push
|
||||
else
|
||||
./pipeline.sh
|
||||
fi
|
||||
Generated
-1
@@ -1890,7 +1890,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"subtle",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"toml",
|
||||
|
||||
@@ -76,7 +76,6 @@ pnet_datalink = "0.35"
|
||||
rand = "0.9"
|
||||
sha1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
subtle = "2.6.1"
|
||||
timeago = { version = "0.5", default-features = false }
|
||||
xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] }
|
||||
|
||||
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
|
||||
# Not using "-x" because we aren't debugging.
|
||||
set -Eeuo pipefail
|
||||
|
||||
# We get unbound var err if we don't set arg
|
||||
arg="${1:-}"
|
||||
|
||||
echo "${registry_password}" | helm registry login -u "${registry_user}" --password-stdin "${registry_host}"
|
||||
helm lint "${chart_dir}"
|
||||
chart_version=$(grep '^version: ' "${chart_dir}/Chart.yaml" | cut -d' ' -f 2)
|
||||
helm package "${chart_dir}"
|
||||
|
||||
# If chart already exists in the chart repository, don't push.
|
||||
if helm pull "oci://${registry_host}/${registry_path}/${chart_name}" --version "${chart_version}" &> /dev/null; then
|
||||
echo -e "\e[31mWARNING: Chart ${chart_name} version ${chart_version} already exists in the repository.\nThis means that the chart's code has not changed, or you forgot to update the version in Chart.yaml.\e[0m"
|
||||
else
|
||||
if [[ $arg == 'push' ]]; then
|
||||
helm push "${chart_name}-${chart_version}.tgz" "oci://${registry_host}/${registry_path}"
|
||||
fi
|
||||
fi
|
||||
@@ -191,7 +191,7 @@ impl RequestHandler for GetCurrentAdminTokenInfoRequest {
|
||||
.admin
|
||||
.metrics_token
|
||||
.as_ref()
|
||||
.is_some_and(|s| s.eq_ct(&self.admin_token))
|
||||
.is_some_and(|s| s == &self.admin_token)
|
||||
{
|
||||
return Ok(GetCurrentAdminTokenInfoResponse(
|
||||
GetAdminTokenInfoResponse {
|
||||
@@ -210,7 +210,7 @@ impl RequestHandler for GetCurrentAdminTokenInfoRequest {
|
||||
.admin
|
||||
.admin_token
|
||||
.as_ref()
|
||||
.is_some_and(|s| s.eq_ct(&self.admin_token))
|
||||
.is_some_and(|s| s == &self.admin_token)
|
||||
{
|
||||
return Ok(GetCurrentAdminTokenInfoResponse(
|
||||
GetAdminTokenInfoResponse {
|
||||
|
||||
@@ -117,14 +117,8 @@ impl AdminApiServer {
|
||||
#[cfg(feature = "metrics")] exporter: PrometheusExporter,
|
||||
) -> Arc<Self> {
|
||||
let cfg = &garage.config.admin;
|
||||
let metrics_token = cfg
|
||||
.metrics_token
|
||||
.as_ref()
|
||||
.map(|token| hash_bearer_token(token.extract_secret()));
|
||||
let admin_token = cfg
|
||||
.admin_token
|
||||
.as_ref()
|
||||
.map(|token| hash_bearer_token(token.extract_secret()));
|
||||
let metrics_token = cfg.metrics_token.as_deref().map(hash_bearer_token);
|
||||
let admin_token = cfg.admin_token.as_deref().map(hash_bearer_token);
|
||||
let metrics_require_token = cfg.metrics_require_token;
|
||||
|
||||
let endpoint = garage.system.netapp.endpoint(ADMIN_RPC_PATH.into());
|
||||
|
||||
+2
-228
@@ -47,26 +47,15 @@ where
|
||||
HI: Iterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
rule.allow_origins.iter().any(|x| wildcard_match(x, origin))
|
||||
rule.allow_origins.iter().any(|x| x == "*" || x == origin)
|
||||
&& rule.allow_methods.iter().any(|x| x == "*" || x == method)
|
||||
&& request_headers.all(|h| {
|
||||
rule.allow_headers
|
||||
.iter()
|
||||
.any(|x| wildcard_match(x, h.as_ref()))
|
||||
.any(|x| x == "*" || x == h.as_ref())
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks whether `candidate` matches the pattern `allowed_wildcard`.
|
||||
#[inline]
|
||||
fn wildcard_match(allowed_wildcard: &String, candidate: &str) -> bool {
|
||||
if allowed_wildcard.contains("*") {
|
||||
let parts = allowed_wildcard.split("*").collect::<Vec<&str>>();
|
||||
parts.len() == 2 && candidate.starts_with(parts[0]) && candidate.ends_with(parts[1])
|
||||
} else {
|
||||
candidate == allowed_wildcard
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_cors_headers(
|
||||
resp: &mut Response<impl Body>,
|
||||
rule: &GarageCorsRule,
|
||||
@@ -201,221 +190,6 @@ pub fn handle_options_for_bucket<B>(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn cors_rule(
|
||||
allow_origins: &[&str],
|
||||
allow_methods: &[&str],
|
||||
allow_headers: &[&str],
|
||||
) -> GarageCorsRule {
|
||||
GarageCorsRule {
|
||||
id: None,
|
||||
max_age_seconds: None,
|
||||
allow_origins: allow_origins.iter().map(|s| s.to_string()).collect(),
|
||||
allow_methods: allow_methods.iter().map(|s| s.to_string()).collect(),
|
||||
allow_headers: allow_headers.iter().map(|s| s.to_string()).collect(),
|
||||
expose_headers: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_when_origin_method_and_headers_are_explicitly_allowed() {
|
||||
let rule = cors_rule(
|
||||
&["https://app.example.test"],
|
||||
&["GET", "PUT"],
|
||||
&["content-type", "x-custom"],
|
||||
);
|
||||
let headers = vec!["content-type", "x-custom"];
|
||||
|
||||
assert!(cors_rule_matches(
|
||||
&rule,
|
||||
"https://app.example.test",
|
||||
"PUT",
|
||||
headers.iter(),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_match_when_origin_is_not_allowed() {
|
||||
let rule = cors_rule(&["https://app.example.test"], &["GET"], &["*"]);
|
||||
|
||||
assert!(!cors_rule_matches(
|
||||
&rule,
|
||||
"https://evil.example.test",
|
||||
"GET",
|
||||
std::iter::empty::<&str>(),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_match_when_method_is_not_allowed() {
|
||||
let rule = cors_rule(&["*"], &["GET"], &["*"]);
|
||||
|
||||
assert!(!cors_rule_matches(
|
||||
&rule,
|
||||
"https://app.example.test",
|
||||
"DELETE",
|
||||
std::iter::empty::<&str>(),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_match_when_a_requested_header_is_not_allowed() {
|
||||
let rule = cors_rule(&["*"], &["GET"], &["content-type"]);
|
||||
let headers = vec!["content-type", "x-not-allowed"];
|
||||
|
||||
assert!(!cors_rule_matches(
|
||||
&rule,
|
||||
"https://app.example.test",
|
||||
"GET",
|
||||
headers.iter(),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wildcard_origin_method_and_headers_match_anything() {
|
||||
let rule = cors_rule(&["*"], &["*"], &["*"]);
|
||||
let headers = vec!["x-anything"];
|
||||
|
||||
assert!(cors_rule_matches(
|
||||
&rule,
|
||||
"https://app.example.test",
|
||||
"DELETE",
|
||||
headers.iter(),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wildcard_origin_regex() {
|
||||
let rule = cors_rule(&["https://*.localhost.com"], &["*"], &["*"]);
|
||||
let headers = vec!["x-anything"];
|
||||
|
||||
assert!(cors_rule_matches(
|
||||
&rule,
|
||||
"https://s3.localhost.com",
|
||||
"DELETE",
|
||||
headers.iter(),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn origin_matching_cases() {
|
||||
// (allow_origins, origin, expect_match)
|
||||
let cases: &[(&[&str], &str, bool)] = &[
|
||||
// exact match
|
||||
(
|
||||
&["https://app.example.test"],
|
||||
"https://app.example.test",
|
||||
true,
|
||||
),
|
||||
(
|
||||
&["https://app.example.test"],
|
||||
"https://other.example.test",
|
||||
false,
|
||||
),
|
||||
// full wildcard
|
||||
(&["*"], "https://anything.example.test", true),
|
||||
// subdomain glob
|
||||
(
|
||||
&["https://*.example.test"],
|
||||
"https://foo.example.test",
|
||||
true,
|
||||
),
|
||||
(&["https://*.example.test"], "https://example.test", false),
|
||||
(
|
||||
&["https://*.example.test"],
|
||||
"http://foo.example.test",
|
||||
false,
|
||||
),
|
||||
// multiple allowed origins, at least one should match
|
||||
(
|
||||
&["https://a.example.test", "https://b.example.test"],
|
||||
"https://b.example.test",
|
||||
true,
|
||||
),
|
||||
// match multiple origins
|
||||
(
|
||||
&["https://a*.example.test", "https://ab*.example.test"],
|
||||
"https://abc.example.test",
|
||||
true,
|
||||
),
|
||||
(
|
||||
&["https://a.example.test", "https://b.example.test"],
|
||||
"https://c.example.test",
|
||||
false,
|
||||
),
|
||||
// at most one '*' in a pattern is allowed
|
||||
(&["https://*.example.*"], "https://a.example.test", false),
|
||||
// domain changed with wildcard
|
||||
(
|
||||
&["https://*example.test"],
|
||||
"https://garageexample.test",
|
||||
true,
|
||||
),
|
||||
// trailing '*' matches any suffix, including the empty string,
|
||||
// so this also matches origins with anything (or nothing) after
|
||||
// "example."
|
||||
(&["https://example.*"], "https://example.test", true),
|
||||
(&["https://*example.test"], "https://example.test", true),
|
||||
(&["https://example.*"], "https://example.", true),
|
||||
];
|
||||
|
||||
for (allow_origins, origin, expect_match) in cases {
|
||||
let rule = cors_rule(allow_origins, &["GET"], &["*"]);
|
||||
let got = cors_rule_matches(&rule, origin, "GET", std::iter::empty::<&str>());
|
||||
assert_eq!(
|
||||
got, *expect_match,
|
||||
"allow_origins={allow_origins:?}, origin={origin:?}: expected match={expect_match}, got {got}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_matching_cases() {
|
||||
// (allow_headers, requested_headers, expect_match)
|
||||
let cases: &[(&[&str], &[&str], bool)] = &[
|
||||
// exact match
|
||||
(&["content-type"], &["content-type"], true),
|
||||
(&["content-type"], &["x-custom"], false),
|
||||
// full wildcard
|
||||
(&["*"], &["x-anything"], true),
|
||||
// no headers requested always matches, regardless of allow_headers
|
||||
(&["content-type"], &[], true),
|
||||
(&[], &[], true),
|
||||
// prefix glob
|
||||
(&["x-amz-*"], &["x-amz-meta-foo"], true),
|
||||
(&["x-amz-*"], &["x-amz-"], true),
|
||||
(&["x-amz-*"], &["x-other"], false),
|
||||
// suffix glob
|
||||
(&["*-meta"], &["foo-meta"], true),
|
||||
(&["*-meta"], &["-meta"], true),
|
||||
(&["*-meta"], &["foo-meta-bar"], false),
|
||||
// multiple allowed headers, at least one should match per requested header
|
||||
(
|
||||
&["content-type", "x-amz-*"],
|
||||
&["content-type", "x-amz-meta-foo"],
|
||||
true,
|
||||
),
|
||||
(&["content-type", "x-amz-*"], &["x-other"], false),
|
||||
// all requested headers must be covered
|
||||
(&["content-type"], &["content-type", "x-custom"], false),
|
||||
// at most one '*' in a pattern is allowed
|
||||
(&["x-*-*"], &["x-a-b"], false),
|
||||
];
|
||||
|
||||
for (allow_headers, requested_headers, expect_match) in cases {
|
||||
let rule = cors_rule(&["*"], &["GET"], allow_headers);
|
||||
let got = cors_rule_matches(
|
||||
&rule,
|
||||
"https://app.example.test",
|
||||
"GET",
|
||||
requested_headers.iter(),
|
||||
);
|
||||
assert_eq!(
|
||||
got, *expect_match,
|
||||
"allow_headers={allow_headers:?}, requested_headers={requested_headers:?}: expected match={expect_match}, got {got}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn bucket_params_with_rule(allow_origins: Vec<&str>) -> BucketParams {
|
||||
let mut bucket_params = BucketParams::default();
|
||||
bucket_params.cors_config.update(
|
||||
|
||||
+1
-2
@@ -307,8 +307,7 @@ async fn cli_command(opt: Opt) -> Result<(), Error> {
|
||||
|
||||
let net_key_hex_str = rpc_secret.ok_or("No RPC secret provided")?;
|
||||
let network_key = NetworkKey::from_slice(
|
||||
&hex::decode(net_key_hex_str.extract_secret())
|
||||
.err_context("Invalid RPC secret key (bad hex)")?[..],
|
||||
&hex::decode(&net_key_hex_str).err_context("Invalid RPC secret key (bad hex)")?[..],
|
||||
)
|
||||
.ok_or("Invalid RPC secret provided (wrong length)")?;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::path::PathBuf;
|
||||
|
||||
use structopt::StructOpt;
|
||||
|
||||
use garage_util::config::{Config, Secret};
|
||||
use garage_util::config::Config;
|
||||
use garage_util::error::Error;
|
||||
|
||||
/// Structure for secret values or paths that are passed as CLI arguments or environment
|
||||
@@ -99,7 +99,7 @@ pub fn fill_secrets(mut config: Config, secrets: Secrets) -> Result<Config, Erro
|
||||
}
|
||||
|
||||
pub(crate) fn fill_secret(
|
||||
config_secret: &mut Option<Secret<String>>,
|
||||
config_secret: &mut Option<String>,
|
||||
config_secret_file: &Option<PathBuf>,
|
||||
cli_secret: &Option<String>,
|
||||
cli_secret_file: &Option<PathBuf>,
|
||||
@@ -110,7 +110,7 @@ pub(crate) fn fill_secret(
|
||||
(Some(_), Some(_)) => {
|
||||
return Err(format!("only one of `{}` and `{}_file` can be set", name, name).into());
|
||||
}
|
||||
(Some(secret), None) => Some(Secret::new(secret.to_string())),
|
||||
(Some(secret), None) => Some(secret.to_string()),
|
||||
(None, Some(file)) => Some(read_secret_file(file, allow_world_readable)?),
|
||||
(None, None) => None,
|
||||
};
|
||||
@@ -132,10 +132,7 @@ pub(crate) fn fill_secret(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_secret_file(
|
||||
file_path: &PathBuf,
|
||||
allow_world_readable: bool,
|
||||
) -> Result<Secret<String>, Error> {
|
||||
fn read_secret_file(file_path: &PathBuf, allow_world_readable: bool) -> Result<String, Error> {
|
||||
if !allow_world_readable {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
@@ -155,7 +152,7 @@ fn read_secret_file(
|
||||
|
||||
// trim_end: allows for use case such as `echo "$(openssl rand -hex 32)" > somefile`.
|
||||
// also editors sometimes add a trailing newline
|
||||
Ok(Secret::new(String::from(secret_buf.trim_end())))
|
||||
Ok(String::from(secret_buf.trim_end()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+1
-1
@@ -137,7 +137,7 @@ impl Garage {
|
||||
info!("Initializing RPC...");
|
||||
let network_key = hex::decode(config.rpc_secret.as_ref().ok_or_message(
|
||||
"rpc_secret value is missing, not present in config file or in environment",
|
||||
)?.extract_secret())
|
||||
)?)
|
||||
.ok()
|
||||
.and_then(|x| NetworkKey::from_slice(&x))
|
||||
.ok_or_message("Invalid RPC secret key: expected 32 bytes of random hex, please check the documentation for requirements")?;
|
||||
|
||||
+1
-1
@@ -115,7 +115,7 @@ impl ConsulDiscovery {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert(
|
||||
"x-consul-token",
|
||||
reqwest::header::HeaderValue::from_str(token.extract_secret())?,
|
||||
reqwest::header::HeaderValue::from_str(token)?,
|
||||
);
|
||||
builder = builder.default_headers(headers);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ lazy_static.workspace = true
|
||||
tracing.workspace = true
|
||||
rand.workspace = true
|
||||
sha2.workspace = true
|
||||
subtle.workspace = true
|
||||
|
||||
chrono.workspace = true
|
||||
rmp-serde.workspace = true
|
||||
|
||||
+4
-35
@@ -90,7 +90,7 @@ pub struct Config {
|
||||
pub allow_world_readable_secrets: bool,
|
||||
|
||||
/// RPC secret key: 32 bytes hex encoded
|
||||
pub rpc_secret: Option<Secret<String>>,
|
||||
pub rpc_secret: Option<String>,
|
||||
/// Optional file where RPC secret key is read from
|
||||
pub rpc_secret_file: Option<PathBuf>,
|
||||
/// Address to bind for RPC
|
||||
@@ -205,37 +205,6 @@ pub struct WebConfig {
|
||||
pub add_host_to_metrics: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
#[serde(transparent)]
|
||||
pub struct Secret<T>(T);
|
||||
|
||||
impl<T> Secret<T> {
|
||||
pub fn new(secret: T) -> Self {
|
||||
Secret(secret)
|
||||
}
|
||||
|
||||
pub fn extract_secret(&self) -> &T {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: std::ops::Deref<Target = str>> Secret<T> {
|
||||
pub fn eq_ct(&self, other: &T) -> bool {
|
||||
use subtle::ConstantTimeEq;
|
||||
self.0
|
||||
.deref()
|
||||
.as_bytes()
|
||||
.ct_eq(other.deref().as_bytes())
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::fmt::Debug for Secret<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Secret").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for the admin and monitoring HTTP API
|
||||
#[derive(Deserialize, Debug, Clone, Default)]
|
||||
pub struct AdminConfig {
|
||||
@@ -243,7 +212,7 @@ pub struct AdminConfig {
|
||||
pub api_bind_addr: Option<UnixOrTCPSocketAddress>,
|
||||
|
||||
/// Bearer token to use to scrape metrics
|
||||
pub metrics_token: Option<Secret<String>>,
|
||||
pub metrics_token: Option<String>,
|
||||
/// File to read metrics token from
|
||||
pub metrics_token_file: Option<PathBuf>,
|
||||
/// Whether to require an access token for accessing the metrics endpoint
|
||||
@@ -251,7 +220,7 @@ pub struct AdminConfig {
|
||||
pub metrics_require_token: bool,
|
||||
|
||||
/// Bearer token to use to access Admin API endpoints
|
||||
pub admin_token: Option<Secret<String>>,
|
||||
pub admin_token: Option<String>,
|
||||
/// File to read admin token from
|
||||
pub admin_token_file: Option<PathBuf>,
|
||||
|
||||
@@ -283,7 +252,7 @@ pub struct ConsulDiscoveryConfig {
|
||||
/// Client TLS key to use when connecting to Consul
|
||||
pub client_key: Option<String>,
|
||||
/// /// Token to use for connecting to consul
|
||||
pub token: Option<Secret<String>>,
|
||||
pub token: Option<String>,
|
||||
/// Skip TLS hostname verification
|
||||
#[serde(default)]
|
||||
pub tls_skip_verify: bool,
|
||||
|
||||
Reference in New Issue
Block a user