Skip to content

Commit

Permalink
Enforce minimum wally version (#57)
Browse files Browse the repository at this point in the history
* Clean(ish) request guard outcome handling + enforce minimum wally version

* Send wally version in cli requests

* Update changelog
  • Loading branch information
magnalite authored Nov 11, 2021
1 parent 610bbdf commit 4f86663
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 32 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* Backend API for package metadata and search ([#46][#46])
* Arch users can now use vendored-libgit2 btw ([#52][#52])
* Frontend can search packages and display package info ([#55][#55])
* Minimum Wally version enforcement by registry ([#57][#57])

[#15]: https://github.com/UpliftGames/wally/issues/15
[#35]: https://github.com/UpliftGames/wally/pull/35
Expand All @@ -17,6 +18,7 @@
[#46]: https://github.com/UpliftGames/wally/pull/46
[#52]: https://github.com/UpliftGames/wally/pull/52
[#55]: https://github.com/UpliftGames/wally/pull/55
[#57]: https://github.com/UpliftGames/wally/pull/57

## 0.2.1 (2021-10-01)
* First iteration of wally frontend. ([#32][#32])
Expand Down
3 changes: 3 additions & 0 deletions src/commands/publish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use crate::{
package_index::PackageIndex, GlobalOptions,
};

const VERSION: &str = env!("CARGO_PKG_VERSION");

/// Publish this project to a registry.
#[derive(Debug, StructOpt)]
pub struct PublishSubcommand {
Expand Down Expand Up @@ -56,6 +58,7 @@ impl PublishSubcommand {
let response = client
.post(api.join("/v1/publish")?)
.header("accept", "application/json")
.header("Wally-Version", VERSION)
.bearer_auth(auth)
.body(contents.data().to_owned())
.send()?;
Expand Down
9 changes: 6 additions & 3 deletions src/package_source/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ use crate::package_source::{PackageContents, PackageSource};

use super::PackageSourceId;

const VERSION: &str = env!("CARGO_PKG_VERSION");

pub struct Registry {
index_url: Url,
auth_token: OnceCell<Option<Arc<str>>>,
Expand Down Expand Up @@ -91,7 +93,7 @@ impl PackageSource for Registry {

let url = self.api_url()?.join(&path)?;

let mut request = self.client.get(url);
let mut request = self.client.get(url).header("Wally-Version", VERSION);

if let Some(token) = self.auth_token()? {
request = request.header(AUTHORIZATION, format!("Bearer {}", token));
Expand All @@ -100,10 +102,11 @@ impl PackageSource for Registry {

if !response.status().is_success() {
bail!(
"Failed to download package {} from registry: {} \nResponse: {}",
"Failed to download package {} from registry: {}\n{} {}",
package_id,
self.api_url()?,
response.status()
response.status(),
response.text()?
);
}

Expand Down
46 changes: 24 additions & 22 deletions wally-registry-backend/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ use rocket::{
};
use serde::{Deserialize, Serialize};

use crate::config::Config;
use crate::error::Error;
use crate::{config::Config, error::ApiErrorStatus};

#[derive(Deserialize, Serialize)]
#[serde(tag = "type", content = "value", rename_all = "kebab-case")]
Expand Down Expand Up @@ -49,29 +50,32 @@ impl fmt::Debug for AuthMode {
}
}

fn match_api_key<T>(request: &Request<'_>, key: &str, result: T) -> Outcome<T, anyhow::Error> {
fn match_api_key<T>(request: &Request<'_>, key: &str, result: T) -> Outcome<T, Error> {
let input_api_key: String = match request.headers().get_one("authorization") {
Some(key) if key.starts_with("Bearer ") => (key[6..].trim()).to_owned(),
_ => {
return Outcome::Failure((Status::Unauthorized, format_err!("API key required")));
return format_err!("API key required")
.status(Status::Unauthorized)
.into();
}
};

if constant_time_eq(key.as_bytes(), input_api_key.as_bytes()) {
Outcome::Success(result)
} else {
Outcome::Failure((
Status::Unauthorized,
format_err!("Invalid API key for read access"),
))
format_err!("Invalid API key for read access")
.status(Status::Unauthorized)
.into()
}
}

async fn verify_github_token(request: &Request<'_>) -> Outcome<WriteAccess, anyhow::Error> {
async fn verify_github_token(request: &Request<'_>) -> Outcome<WriteAccess, Error> {
let token: String = match request.headers().get_one("authorization") {
Some(key) if key.starts_with("Bearer ") => (key[6..].trim()).to_owned(),
_ => {
return Outcome::Failure((Status::Unauthorized, format_err!("Github auth required")));
return format_err!("Github auth required")
.status(Status::Unauthorized)
.into();
}
};

Expand All @@ -86,16 +90,15 @@ async fn verify_github_token(request: &Request<'_>) -> Outcome<WriteAccess, anyh

let github_info = match response {
Err(err) => {
return Outcome::Failure((Status::InternalServerError, format_err!(err)));
return format_err!(err).status(Status::InternalServerError).into();
}
Ok(response) => response.json::<GithubInfo>().await,
};

match github_info {
Err(err) => Outcome::Failure((
Status::Unauthorized,
format_err!("Github auth failed: {}", err),
)),
Err(err) => format_err!("Github auth failed: {}", err)
.status(Status::Unauthorized)
.into(),
Ok(github_info) => Outcome::Success(WriteAccess::Github(github_info)),
}
}
Expand All @@ -107,9 +110,9 @@ pub enum ReadAccess {

#[rocket::async_trait]
impl<'r> FromRequest<'r> for ReadAccess {
type Error = anyhow::Error;
type Error = Error;

async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Error> {
let config = request
.guard::<State<Config>>()
.await
Expand Down Expand Up @@ -156,19 +159,18 @@ impl WriteAccess {

#[rocket::async_trait]
impl<'r> FromRequest<'r> for WriteAccess {
type Error = anyhow::Error;
type Error = Error;

async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Error> {
let config = request
.guard::<State<Config>>()
.await
.expect("AuthMode was not configured");

match &config.auth {
AuthMode::Unauthenticated => Outcome::Failure((
Status::Unauthorized,
format_err!("Invalid API key for write access"),
)),
AuthMode::Unauthenticated => format_err!("Invalid API key for write access")
.status(Status::Unauthorized)
.into(),
AuthMode::ApiKey(key) => match_api_key(request, key, WriteAccess::ApiKey),
AuthMode::DoubleApiKey { write, .. } => {
match_api_key(request, write, WriteAccess::ApiKey)
Expand Down
4 changes: 4 additions & 0 deletions wally-registry-backend/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use semver::Version;
use serde::{Deserialize, Serialize};
use url::Url;

Expand All @@ -18,4 +19,7 @@ pub struct Config {

/// Which storage backend to use.
pub storage: StorageMode,

/// The minimum wally cli version required to publish to the registry
pub minimum_wally_version: Option<Version>,
}
12 changes: 12 additions & 0 deletions wally-registry-backend/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use std::io::Cursor;

use rocket::{
http::{ContentType, Status},
outcome::Outcome::Failure,
request::Outcome,
response::Responder,
Request, Response,
};
Expand Down Expand Up @@ -38,6 +40,7 @@ where
/// Error type returned by most API endpoints. This type has automatic
/// conversions from pretty much any error type and uses an `anyhow::Error`
/// internally.
#[derive(Debug)]
pub struct Error {
message: String,
status: Status,
Expand Down Expand Up @@ -67,6 +70,15 @@ where
}
}

impl<S, E> From<Error> for Outcome<S, E>
where
E: From<Error>,
{
fn from(err: Error) -> Self {
Failure((Status::InternalServerError, err.into()))
}
}

impl<'r> Responder<'r, 'static> for Error {
fn respond_to(self, _request: &'r Request<'_>) -> rocket::response::Result<'static> {
let response = ErrorResponse {
Expand Down
77 changes: 70 additions & 7 deletions wally-registry-backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use libwally::{
};
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::Header;
use rocket::request::{FromRequest, Outcome};
use rocket::{
data::{Data, ToByteUnit},
fairing::AdHoc,
Expand Down Expand Up @@ -57,11 +58,15 @@ fn root() -> Json<serde_json::Value> {
#[get("/v1/package-contents/<scope>/<name>/<version>")]
async fn package_contents(
storage: State<'_, Box<dyn StorageBackend>>,
_read: ReadAccess,
_read: Result<ReadAccess, Error>,
scope: String,
name: String,
version: String,
_cli_version: Result<WallyVersion, Error>,
) -> Result<Content<Stream<StorageOutput>>, Error> {
_read?;
_cli_version?;

let package_name = PackageName::new(scope, name)
.context("error parsing package name")
.status(Status::BadRequest)?;
Expand All @@ -80,10 +85,12 @@ async fn package_contents(
#[get("/v1/package-metadata/<scope>/<name>")]
async fn package_info(
index: State<'_, PackageIndex>,
_read: ReadAccess,
_read: Result<ReadAccess, Error>,
scope: String,
name: String,
) -> Result<Json<serde_json::Value>, Error> {
_read?;

let package_name = PackageName::new(scope, name)
.context("error parsing package name")
.status(Status::BadRequest)?;
Expand All @@ -96,9 +103,11 @@ async fn package_info(
#[get("/v1/package-search?<query>")]
async fn package_search(
search_backend: State<'_, RwLock<SearchBackend>>,
_read: ReadAccess,
_read: Result<ReadAccess, Error>,
query: String,
) -> Result<Json<serde_json::Value>, Error> {
_read?;

if let Ok(search_backend) = search_backend.read() {
let result = search_backend.search(&query)?;
Ok(Json(serde_json::to_value(result)?))
Expand All @@ -115,9 +124,13 @@ async fn publish(
storage: State<'_, Box<dyn StorageBackend>>,
search_backend: State<'_, RwLock<SearchBackend>>,
index: State<'_, PackageIndex>,
authorization: WriteAccess,
authorization: Result<WriteAccess, Error>,
_cli_version: Result<WallyVersion, Error>,
data: Data,
) -> Result<Json<serde_json::Value>, Error> {
_cli_version?;
let authorization = authorization?;

let contents = data
.open(2.mebibytes())
.into_bytes()
Expand Down Expand Up @@ -151,8 +164,8 @@ async fn publish(
let user_id = github_info.id();
let scope = package_id.name().scope();

if !index.is_scope_owner(&scope, &user_id)? {
index.add_scope_owner(&scope, &user_id)?;
if !index.is_scope_owner(scope, user_id)? {
index.add_scope_owner(scope, user_id)?;
}
}

Expand Down Expand Up @@ -274,7 +287,57 @@ impl Fairing for Cors {
response.set_header(Header::new("Access-Control-Allow-Origin", "*"));
response.set_header(Header::new("Access-Control-Allow-Methods", "GET"));
response.set_header(Header::new("Access-Control-Allow-Headers", "*"));
response.set_header(Header::new("Access-Control-Allow-Credentials", "true"));
}
}

struct WallyVersion;

#[rocket::async_trait]
impl<'r> FromRequest<'r> for WallyVersion {
type Error = Error;

async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let config = request
.guard::<State<Config>>()
.await
.expect("Failed to load config");

let minimum_version = match &config.minimum_wally_version {
Some(version) => version,
None => return Outcome::Success(WallyVersion),
};

let version = match request.headers().get_one("Wally-Version") {
Some(version) => version,
None => {
return format_err!(
"Wally version header required. Try upgrading your wally installation."
)
.status(Status::BadRequest)
.into();
}
};

let version = match Version::parse(version) {
Ok(version) => version,
Err(err) => {
return format_err!("Failed to parse wally version header: {}", err)
.status(Status::BadRequest)
.into();
}
};

if &version < minimum_version {
format_err!(
"This registry requires Wally {} (you are using {})",
minimum_version,
version
)
.status(Status::BadRequest)
.into()
} else {
Outcome::Success(WallyVersion)
}
}
}

Expand Down
1 change: 1 addition & 0 deletions wally-registry-backend/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ fn new_client_with_remote(auth: AuthMode, index_url: url::Url) -> Client {
},
auth,
github_token: None,
minimum_wally_version: None,
}));

Client::tracked(server(figment)).expect("valid rocket instance")
Expand Down

0 comments on commit 4f86663

Please sign in to comment.