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
421 changes: 391 additions & 30 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ bollard = "0.16.1"
tar = "0.4.42"
tempfile = "3.13.0"
figment = { version = "0.10.19", features = ["env", "yaml", "test"] }
rust-s3 = { version = "0.35.1", default-features = false, features = [
"fail-on-err",
"tokio-rustls-tls",
] }


[dev-dependencies]
pretty_assertions = "1.4.1"
93 changes: 90 additions & 3 deletions src/access_handlers/s3.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,96 @@
use anyhow::{Error, Result};
use anyhow::{anyhow, bail, Context, Error, Result};
use s3;
use simplelog::*;
use tokio;

use crate::configparser::{get_config, get_profile_config};
use crate::configparser::{
config::{ProfileConfig, S3Config},
get_config, get_profile_config,
};

/// s3 bucket access checks
pub fn check(profile_name: &str) -> Result<()> {
#[tokio::main(flavor = "current_thread")] // make this a sync function
pub async fn check(profile_name: &str) -> Result<()> {
let profile = get_profile_config(profile_name)?;

let bucket = bucket_client(&profile.s3)?;

if !bucket.exists().await? {
bail!("bucket {} does not exist!", profile.s3.bucket_name);
}

// try uploading file to bucket
debug!("uploading test file to bucket");
let test_file = ("/beavercds-test-file", "access test file!");
bucket
.put_object_with_content_type(test_file.0, test_file.1.as_bytes(), "text/plain")
.await
.with_context(|| {
format!(
"could not upload to asset bucket {:?}",
profile.s3.bucket_name
)
})?;

// download it to check
debug!("downloading test file");
let from_bucket = bucket.get_object(test_file.0).await?;
if from_bucket.bytes() != test_file.1 {
bail!("uploaded test file contents do not match, somehow!?");
}

// download as anonymous to check public access
debug!("downloading test file as public user");
let public_bucket = bucket_client_anonymous(&profile.s3)?;
let from_public = public_bucket
.get_object(test_file.0)
.await
.with_context(|| {
anyhow!(
"public download from asset bucket {:?} failed",
profile.s3.bucket_name
)
})?;
if from_public.bytes() != test_file.1 {
bail!("contents of public bucket do not match uploaded file");
}

// clean up test file after checks
bucket.delete_object(test_file.0).await?;

Ok(())
}

/// create bucket client for passed profile config
pub fn bucket_client(config: &S3Config) -> Result<Box<s3::Bucket>> {
trace!("creating bucket client");
// TODO: once_cell this so it reuses the same bucket?
let region = s3::Region::Custom {
region: config.region.clone(),
endpoint: config.endpoint.clone(),
};
let creds = s3::creds::Credentials::new(
Some(&config.access_key),
Some(&config.secret_key),
None,
None,
None,
)?;
let bucket = s3::Bucket::new(&config.bucket_name, region, creds)?.with_path_style();

Ok(bucket)
}

/// create public/anonymous bucket client for passed profile config
pub fn bucket_client_anonymous(config: &S3Config) -> Result<Box<s3::Bucket>> {
trace!("creating anon bucket client");
// TODO: once_cell this so it reuses the same bucket?
let region = s3::Region::Custom {
region: config.region.clone(),
endpoint: config.endpoint.clone(),
};
let creds = s3::creds::Credentials::anonymous()?;
let bucket = s3::Bucket::new(&config.bucket_name, region, creds)?.with_path_style();

Ok(bucket)
}
3 changes: 3 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,8 @@ pub enum Commands {
/// Check container registry access and permissions
#[arg(short, long)]
registry: bool,

#[arg(short, long, help = "Check S3 asset bucket access and permissions")]
bucket: bool,
},
}
75 changes: 52 additions & 23 deletions src/commands/check_access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,50 @@ use std::process::exit;
use crate::access_handlers as access;
use crate::configparser::{get_config, get_profile_config};

pub fn run(profile: &str, kubernetes: &bool, frontend: &bool, registry: &bool) {
pub fn run(profile: &str, kubernetes: &bool, frontend: &bool, registry: &bool, bucket: &bool) {
// if user did not give a specific check, check all of them
let check_all = !kubernetes && !frontend && !registry;
let check_all = !kubernetes && !frontend && !registry && !bucket;

let config = get_config().unwrap();

let to_check: Vec<_> = match profile {
let profiles_to_check: Vec<_> = match profile {
"all" => config.profiles.keys().cloned().collect(),
p => vec![String::from(p)],
};

let results: Result<(), Vec<_>> = to_check.into_iter().try_for_each(|p| {
check_profile(
&p,
*kubernetes || check_all,
*frontend || check_all,
*registry || check_all,
)
});
let results: Vec<_> = profiles_to_check
.iter()
.map(|profile_name| {
(
profile_name, // associate profile name to results
check_profile(
profile_name,
*kubernetes || check_all,
*frontend || check_all,
*registry || check_all,
*bucket || check_all,
),
)
})
.collect();

debug!("access results: {results:?}");

// die if there were any errors
match results {
Ok(_) => info!(" all good!"),
Err(errs) => {
error!("Error checking profile {profile}:");
errs.iter().for_each(|e| error!("{e:?}\n"));
exit(1)
let mut should_exit = false;
for (profile, result) in results.iter() {
match result {
Ok(_) => info!(" all good!"),
Err(errs) => {
error!("{} errors checking profile {profile}:", errs.len());
errs.iter().for_each(|e| error!("{e:?}\n"));
should_exit = true
}
}
}
if should_exit {
exit(1);
}
}

/// checks a single profile (`profile`) for the given accesses
Expand All @@ -44,22 +59,36 @@ fn check_profile(
kubernetes: bool,
frontend: bool,
registry: bool,
bucket: bool,
) -> Result<(), Vec<Error>> {
info!("checking profile {name}...");

let mut results = vec![];
let mut errs = vec![];

if kubernetes {
results.push(access::kube::check(name).context("could not access kubernetes cluster"));
match access::kube::check(name).context("could not access kubernetes cluster") {
Err(e) => errs.push(e),
Ok(_) => info!(" kubernetes ok!"),
};
}
if frontend {
results.push(access::frontend::check(name).context("could not access frontend"));
match access::frontend::check(name).context("could not access frontend") {
Err(e) => errs.push(e),
Ok(_) => info!(" frontend ok!"),
};
}
if registry {
results.push(access::docker::check(name).context("could not access container registry"));
match access::docker::check(name).context("could not access container registry") {
Err(e) => errs.push(e),
Ok(_) => info!(" registry ok!"),
};
}
if bucket {
match access::s3::check(name).context("could not access asset bucket") {
Err(e) => errs.push(e),
Ok(_) => info!(" bucket ok!"),
};
}

let (ok, errs): (Vec<_>, Vec<_>) = results.into_iter().partition_result();

if !errs.is_empty() {
Err(errs)
Expand Down
20 changes: 16 additions & 4 deletions src/configparser/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ pub fn parse() -> Result<RcdsConfig> {
debug!("trying to parse rcds.yaml");

let env_overrides = Env::prefixed("BEAVERCDS_").split("_").map(|var| {
// Using "_" as the split character works for almost all of our keys.
// but some of the profile settings keys have underscores as part of the
// key. This handles those few keys by undoing the s/_/./ that the
// Figment split() did.
// Using "_" as the split character works for almost all of our keys,
// but some profile settings have underscores. This handles those few
// keys by undoing the s/_/./ that the figment::split() did.
var.to_string()
.to_lowercase()
.replace("frontend.", "frontend_")
.replace("challenges.", "challenges_")
.replace("s3.access.", "s3.access_")
.replace("s3.secret.", "s3.secret_")
.into()
});
trace!(
Expand Down Expand Up @@ -101,6 +102,7 @@ struct ProfileConfig {
challenges_domain: String,
kubeconfig: Option<String>,
kubecontext: String,
s3: S3Config,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
Expand All @@ -110,3 +112,13 @@ struct ChallengePoints {
min: i64,
max: i64,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[fully_pub]
struct S3Config {
bucket_name: String,
endpoint: String,
region: String,
access_key: String,
secret_key: String,
}
3 changes: 2 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ fn main() {
kubernetes,
frontend,
registry,
bucket,
} => {
commands::validate::run();
commands::check_access::run(profile, kubernetes, frontend, registry)
commands::check_access::run(profile, kubernetes, frontend, registry, bucket)
}

#[allow(unused_variables)]
Expand Down
22 changes: 11 additions & 11 deletions src/tests/parsing/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,13 @@ fn all_yaml() {
challenges_domain: "chals.frontend.example".to_string(),
kubeconfig: None,
kubecontext: "testcluster".to_string(),
// s3: S3Config {
// bucket_name: "asset_testing".to_string(),
// endpoint: "s3.example".to_string(),
// region: "us-fake-1".to_string(),
// access_key: "accesskey".to_string(),
// secret_key: "secretkey".to_string(),
// }
s3: S3Config {
bucket_name: "asset_testing".to_string(),
endpoint: "s3.example".to_string(),
region: "us-fake-1".to_string(),
access_key: "accesskey".to_string(),
secret_key: "secretkey".to_string(),
},
},
)]),
};
Expand Down Expand Up @@ -193,8 +193,8 @@ fn yaml_with_env_overrides() {
let profile = config.profiles.get("testing").unwrap();

assert_eq!(profile.frontend_token, "envtoken");
// assert_eq!(profile.s3.access_key, "envkey");
// assert_eq!(profile.s3.secret_key, "envsecret");
assert_eq!(profile.s3.access_key, "envkey");
assert_eq!(profile.s3.secret_key, "envsecret");

Ok(())
});
Expand Down Expand Up @@ -263,8 +263,8 @@ fn partial_yaml_with_env() {
let profile = config.profiles.get("testing").unwrap();

assert_eq!(profile.frontend_token, "envtoken");
// assert_eq!(profile.s3.access_key, "envkey");
// assert_eq!(profile.s3.secret_key, "envsecret");
assert_eq!(profile.s3.access_key, "envkey");
assert_eq!(profile.s3.secret_key, "envsecret");

Ok(())
});
Expand Down
30 changes: 30 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Running Tests

Since this needs to interact with a container registry, S3 storage, and K8S,
there is some extra setup needed before running `cargo test` or running against
the test chals repo.

## `setup.sh`

Main setup script. Run or source this file to set up infrastructure.
Recommended to source this file to set the config override environment
environment variables for test tokens and addresses.

Spins up a local Minikube K8S cluster and other test environment components via
Docker Compose.

```sh
source tests/setup.sh up
source tests/setup.sh down
```

## `services.compose.yaml`

Non-K8S resources required to run tests against:
- Container registry
- S3 buckets (via Minio)

## `repo/`

Example challenges repo to test against. Contains a variety of challenge types:
static file only (garf), HTTP web (bar), and TCP pwn (notsh).
17 changes: 0 additions & 17 deletions tests/docker-compose.testregistry.yaml

This file was deleted.

6 changes: 3 additions & 3 deletions tests/repo/rcds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ profiles:
challenges_domain: chals.frontend.example
kubecontext: testcluster
s3:
# local minio
bucket_name: testbucket
endpoint: localhost:9000
region: x
accessKey: accesskey
secretAccessKey: secretkey
access_key: somekey
secret_key: somesecret
Loading
Loading