introduce AuthConfig v2, which removes role emulation (#891)

<!-- The PR description should answer 2 (maybe 3) important questions:
-->

### What

We have decided to remove the role emulation feature from engine
altogether.

More details in the RFC -
https://docs.google.com/document/d/1tlS9pqRzLEotLXN_dhjFOeIgbH6zmejOdZTbkkPD-aM/edit

### How

<!-- How is it trying to accomplish it (what are the implementation
steps)? -->

V3_GIT_ORIGIN_REV_ID: e7cb765df5afac6c6d6a05a572a832ce9910cc0b
This commit is contained in:
Anon Ray 2024-07-29 14:34:21 +05:30 committed by hasura-bot
parent a8d1002175
commit 5b23ed53bc
5 changed files with 181 additions and 85 deletions

View File

@ -12,6 +12,9 @@
### Changed ### Changed
- Introduced `AuthConfig` `v2`. This new version removes role emulation in
engine (`allowRoleEmulationBy`) field.
## [v2024.07.25] ## [v2024.07.25]
### Fixed ### Fixed

View File

@ -36,8 +36,8 @@ fn build_allowed_roles(
pub async fn authenticate_request( pub async fn authenticate_request(
http_client: &reqwest::Client, http_client: &reqwest::Client,
jwt_config: JWTConfig, jwt_config: JWTConfig,
allow_role_emulation_for: Option<&Role>,
headers: &HeaderMap, headers: &HeaderMap,
allow_role_emulation_for: Option<&Role>,
) -> Result<Identity, Error> { ) -> Result<Identity, Error> {
let tracer = tracing_util::global_tracer(); let tracer = tracing_util::global_tracer();
tracer tracer
@ -213,8 +213,8 @@ mod tests {
let authenticated_identity = authenticate_request( let authenticated_identity = authenticate_request(
&http_client, &http_client,
jwt_config, jwt_config,
Some(&Role::new("admin")),
&header_map, &header_map,
Some(&Role::new("admin")),
) )
.await?; .await?;
@ -310,8 +310,8 @@ mod tests {
let authenticated_identity = authenticate_request( let authenticated_identity = authenticate_request(
&http_client, &http_client,
jwt_config, jwt_config,
Some(&Role::new("admin")),
&header_map, &header_map,
Some(&Role::new("admin")),
) )
.await?; .await?;

View File

@ -1,4 +1,6 @@
use std::fmt::Display; use std::fmt::Display;
use std::hash;
use std::hash::{Hash, Hasher};
use std::net; use std::net;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
@ -12,29 +14,18 @@ use axum::{
routing::{get, post}, routing::{get, post},
Extension, Json, Router, Extension, Json, Router,
}; };
use base64::engine::Engine;
use clap::Parser; use clap::Parser;
use pre_execution_plugin::{
configuration::PrePluginConfig, execute::pre_execution_plugins_handler,
};
use reqwest::header::CONTENT_TYPE; use reqwest::header::CONTENT_TYPE;
use serde::Serialize; use serde::Serialize;
use tower_http::cors::CorsLayer; use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
use tracing_util::{
add_event_on_active_span, set_attribute_on_active_span, set_status_on_current_span,
ErrorVisibility, SpanVisibility, TraceableError, TraceableHttpResponse,
};
use base64::engine::Engine;
use engine::internal_flags::{resolve_unstable_features, UnstableFeature};
use engine::VERSION;
use engine::{ use engine::{
authentication::{ authentication::{resolve_auth_config, AuthConfig, AuthModeConfig},
AuthConfig::{self, V1 as V1AuthConfig}, internal_flags::{resolve_unstable_features, UnstableFeature},
AuthModeConfig,
},
plugins::read_pre_execution_plugins_config, plugins::read_pre_execution_plugins_config,
VERSION,
}; };
use execute::HttpContext; use execute::HttpContext;
use hasura_authn_core::Session; use hasura_authn_core::Session;
@ -43,9 +34,14 @@ use hasura_authn_jwt::jwt;
use hasura_authn_noauth as noauth; use hasura_authn_noauth as noauth;
use hasura_authn_webhook::webhook; use hasura_authn_webhook::webhook;
use lang_graphql as gql; use lang_graphql as gql;
use pre_execution_plugin::{
configuration::PrePluginConfig, execute::pre_execution_plugins_handler,
};
use schema::GDS; use schema::GDS;
use std::hash; use tracing_util::{
use std::hash::{Hash, Hasher}; add_event_on_active_span, set_attribute_on_active_span, set_status_on_current_span,
ErrorVisibility, SpanVisibility, TraceableError, TraceableHttpResponse,
};
mod cors; mod cors;
@ -547,33 +543,36 @@ where
SpanVisibility::Internal, SpanVisibility::Internal,
|| { || {
Box::pin(async { Box::pin(async {
match &engine_state.auth_config { // We are still supporting AuthConfig::V1, hence we need to
V1AuthConfig(auth_config) => match &auth_config.mode { // support role emulation
AuthModeConfig::NoAuth(no_auth_config) => { let (auth_mode, allow_role_emulation_by) = match &engine_state.auth_config {
Ok(noauth::identity_from_config(no_auth_config)) AuthConfig::V1(auth_config) => (
} &auth_config.mode,
auth_config.allow_role_emulation_by.as_ref(),
AuthModeConfig::Webhook(webhook_config) => { ),
webhook::authenticate_request( // There is no role emulation in AuthConfig::V2
&engine_state.http_context.client, AuthConfig::V2(auth_config) => (&auth_config.mode, None),
webhook_config, };
&headers_map, match auth_mode {
auth_config.allow_role_emulation_by.as_ref(), AuthModeConfig::NoAuth(no_auth_config) => {
) Ok(noauth::identity_from_config(no_auth_config))
.await }
.map_err(AuthError::from) AuthModeConfig::Webhook(webhook_config) => webhook::authenticate_request(
} &engine_state.http_context.client,
AuthModeConfig::Jwt(jwt_secret_config) => { webhook_config,
jwt_auth::authenticate_request( &headers_map,
&engine_state.http_context.client, allow_role_emulation_by,
*jwt_secret_config.clone(), )
auth_config.allow_role_emulation_by.as_ref(), .await
&headers_map, .map_err(AuthError::from),
) AuthModeConfig::Jwt(jwt_secret_config) => jwt_auth::authenticate_request(
.await &engine_state.http_context.client,
.map_err(AuthError::from) *jwt_secret_config.clone(),
} &headers_map,
}, allow_role_emulation_by,
)
.await
.map_err(AuthError::from),
} }
}) })
}, },
@ -728,7 +727,7 @@ async fn handle_sql_request(
#[allow(clippy::print_stdout)] #[allow(clippy::print_stdout)]
/// Print any build warnings to stdout /// Print any build warnings to stdout
fn print_warnings(warnings: Vec<metadata_resolve::Warning>) { fn print_warnings<T: Display>(warnings: Vec<T>) {
for warning in warnings { for warning in warnings {
println!("Warning: {warning}"); println!("Warning: {warning}");
} }
@ -742,15 +741,23 @@ fn build_state(
pre_execution_plugins_path: &Option<PathBuf>, pre_execution_plugins_path: &Option<PathBuf>,
metadata_resolve_configuration: metadata_resolve::configuration::Configuration, metadata_resolve_configuration: metadata_resolve::configuration::Configuration,
) -> Result<Arc<EngineState>, anyhow::Error> { ) -> Result<Arc<EngineState>, anyhow::Error> {
let auth_config = read_auth_config(authn_config_path).map_err(StartupError::ReadAuth)?; // Auth Config
let raw_auth_config = std::fs::read_to_string(authn_config_path)?;
let (auth_config, auth_warnings) =
resolve_auth_config(&raw_auth_config).map_err(StartupError::ReadAuth)?;
// Plugins
let pre_execution_plugins_config = let pre_execution_plugins_config =
read_pre_execution_plugins_config(pre_execution_plugins_path) read_pre_execution_plugins_config(pre_execution_plugins_path)
.map_err(StartupError::ReadPrePlugin)?; .map_err(StartupError::ReadPrePlugin)?;
// Metadata
let raw_metadata = std::fs::read_to_string(metadata_path)?; let raw_metadata = std::fs::read_to_string(metadata_path)?;
let metadata = open_dds::Metadata::from_json_str(&raw_metadata)?; let metadata = open_dds::Metadata::from_json_str(&raw_metadata)?;
let (resolved_metadata, warnings) = let (resolved_metadata, warnings) =
metadata_resolve::resolve(metadata, metadata_resolve_configuration)?; metadata_resolve::resolve(metadata, metadata_resolve_configuration)?;
print_warnings(auth_warnings);
print_warnings(warnings); print_warnings(warnings);
let http_context = HttpContext { let http_context = HttpContext {
@ -772,10 +779,3 @@ fn build_state(
}); });
Ok(state) Ok(state)
} }
fn read_auth_config(path: &PathBuf) -> Result<AuthConfig, anyhow::Error> {
let raw_auth_config = std::fs::read_to_string(path)?;
Ok(open_dds::traits::OpenDd::deserialize(
serde_json::from_str(&raw_auth_config)?,
)?)
}

View File

@ -25,6 +25,42 @@ pub enum AuthModeConfig {
/// Definition of the authentication configuration used by the API server. /// Definition of the authentication configuration used by the API server.
pub enum AuthConfig { pub enum AuthConfig {
V1(AuthConfigV1), V1(AuthConfigV1),
V2(AuthConfigV2),
}
impl AuthConfig {
pub fn upgrade(self) -> AuthConfigV2 {
match self {
AuthConfig::V1(v1) => AuthConfigV2 { mode: v1.mode },
AuthConfig::V2(v2) => v2,
}
}
}
#[derive(Serialize, Debug, Clone, JsonSchema, PartialEq, opendds_derive::OpenDd, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
#[schemars(title = "AuthConfigV2")]
#[schemars(example = "AuthConfigV2::example")]
/// Definition of the authentication configuration used by the API server.
pub struct AuthConfigV2 {
pub mode: AuthModeConfig,
}
impl AuthConfigV2 {
fn example() -> Self {
open_dds::traits::OpenDd::deserialize(serde_json::json!(
{
"mode": {
"webhook": {
"url": "http://auth_hook:3050/validate-request",
"method": "Post"
}
}
}
))
.unwrap()
}
} }
#[derive(Serialize, Debug, Clone, JsonSchema, PartialEq, opendds_derive::OpenDd, Deserialize)] #[derive(Serialize, Debug, Clone, JsonSchema, PartialEq, opendds_derive::OpenDd, Deserialize)]
@ -55,6 +91,29 @@ impl AuthConfigV1 {
} }
} }
/// Warnings for the user raised during auth config generation
/// These are things that don't break the build, but may do so in future
#[derive(Debug, thiserror::Error)]
pub enum Warning {
#[error("AuthConfig v1 is deprecated. `allowRoleEmulationBy` has been removed. Please consider upgrading to AuthConfig v2.")]
PleaseUpgradeToV2,
}
/// Resolve `AuthConfig` which is not part of metadata. Hence we resolve/build
/// it separately. This also emits warnings.
pub fn resolve_auth_config(
raw_auth_config: &str,
) -> Result<(AuthConfig, Vec<Warning>), anyhow::Error> {
let mut warnings = vec![];
let auth_config: AuthConfig =
open_dds::traits::OpenDd::deserialize(serde_json::from_str(raw_auth_config)?)?;
match &auth_config {
AuthConfig::V1(_) => warnings.push(Warning::PleaseUpgradeToV2),
AuthConfig::V2(_) => (),
}
Ok((auth_config, warnings))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use goldenfile::Mint; use goldenfile::Mint;

View File

@ -22,6 +22,25 @@
} }
}, },
"additionalProperties": false "additionalProperties": false
},
{
"type": "object",
"required": [
"definition",
"version"
],
"properties": {
"version": {
"type": "string",
"enum": [
"v2"
]
},
"definition": {
"$ref": "#/definitions/AuthConfigV2"
}
},
"additionalProperties": false
} }
], ],
"definitions": { "definitions": {
@ -319,39 +338,17 @@
} }
] ]
}, },
"NoAuthConfig": { "AuthConfigV2": {
"$id": "https://hasura.io/jsonschemas/metadata/NoAuthConfig", "$id": "https://hasura.io/jsonschemas/metadata/AuthConfigV2",
"title": "NoAuthConfig", "title": "AuthConfigV2",
"description": "Configuration used when running engine without authentication", "description": "Definition of the authentication configuration used by the API server.",
"examples": [
{
"role": "admin",
"sessionVariables": {
"x-hasura-user-id": "100"
}
}
],
"type": "object", "type": "object",
"required": [ "required": [
"role", "mode"
"sessionVariables"
], ],
"properties": { "properties": {
"role": { "mode": {
"description": "role to assume whilst running the engine", "$ref": "#/definitions/AuthModeConfig"
"allOf": [
{
"$ref": "#/definitions/Role"
}
]
},
"sessionVariables": {
"title": "SessionVariables",
"description": "static session variables to use whilst running the engine",
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/SessionVariableValue"
}
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -803,6 +800,43 @@
"additionalProperties": false "additionalProperties": false
} }
] ]
},
"NoAuthConfig": {
"$id": "https://hasura.io/jsonschemas/metadata/NoAuthConfig",
"title": "NoAuthConfig",
"description": "Configuration used when running engine without authentication",
"examples": [
{
"role": "admin",
"sessionVariables": {
"x-hasura-user-id": "100"
}
}
],
"type": "object",
"required": [
"role",
"sessionVariables"
],
"properties": {
"role": {
"description": "role to assume whilst running the engine",
"allOf": [
{
"$ref": "#/definitions/Role"
}
]
},
"sessionVariables": {
"title": "SessionVariables",
"description": "static session variables to use whilst running the engine",
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/SessionVariableValue"
}
}
},
"additionalProperties": false
} }
} }
} }