[ENG-643] Support array-valued session variables (#1221)

<!-- The PR description should answer 2 important questions: -->

### What

Allows JSON arrays in session variables.

### How

If the type of a session variable isn't special (custom scalar, string,
or ID), then we parse the header as JSON, and switch into
type-_checking_ mode, returning the JSON wholesale if the type matches.

V3_GIT_ORIGIN_REV_ID: 386558898a188c06b73270a90020a20ff963f5a0
This commit is contained in:
Phil Freeman 2024-10-17 12:13:17 -07:00 committed by hasura-bot
parent a0b998ad7c
commit 16118760fd
37 changed files with 640 additions and 118 deletions

View File

@ -4,6 +4,8 @@
### Added ### Added
- Support array values in session variables
### Fixed ### Fixed
### Changed ### Changed

View File

@ -23,7 +23,7 @@ use std::{
// Session variable and role are defined as part of OpenDD // Session variable and role are defined as part of OpenDD
pub use open_dds::{ pub use open_dds::{
permissions::Role, permissions::Role,
session_variables::{SessionVariable, SESSION_VARIABLE_ROLE}, session_variables::{SessionVariableName, SessionVariableReference, SESSION_VARIABLE_ROLE},
}; };
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, JsonSchema)] #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, JsonSchema)]
@ -37,10 +37,10 @@ impl SessionVariableValue {
} }
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
pub struct SessionVariables(HashMap<SessionVariable, SessionVariableValue>); pub struct SessionVariables(HashMap<SessionVariableName, SessionVariableValue>);
impl SessionVariables { impl SessionVariables {
pub fn get(&self, session_variable: &SessionVariable) -> Option<&SessionVariableValue> { pub fn get(&self, session_variable: &SessionVariableName) -> Option<&SessionVariableValue> {
self.0.get(session_variable) self.0.get(session_variable)
} }
} }
@ -56,14 +56,14 @@ pub struct Session {
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct RoleAuthorization { pub struct RoleAuthorization {
pub role: Role, pub role: Role,
pub session_variables: HashMap<SessionVariable, SessionVariableValue>, pub session_variables: HashMap<SessionVariableName, SessionVariableValue>,
pub allowed_session_variables_from_request: SessionVariableList, pub allowed_session_variables_from_request: SessionVariableList,
} }
impl RoleAuthorization { impl RoleAuthorization {
pub fn build_session( pub fn build_session(
&self, &self,
mut variables: HashMap<SessionVariable, SessionVariableValue>, mut variables: HashMap<SessionVariableName, SessionVariableValue>,
) -> Session { ) -> Session {
let allowed_client_session_variables = match &self.allowed_session_variables_from_request { let allowed_client_session_variables = match &self.allowed_session_variables_from_request {
SessionVariableList::All => variables, SessionVariableList::All => variables,
@ -87,7 +87,7 @@ pub enum SessionVariableList {
// like * in Select * from ... // like * in Select * from ...
All, All,
// An explicit list // An explicit list
Some(HashSet<SessionVariable>), Some(HashSet<SessionVariableName>),
} }
// Privileges of the current user // Privileges of the current user
@ -183,7 +183,7 @@ pub fn authorize_identity(
let mut role = None; let mut role = None;
// traverse through the headers and collect role and session variables // traverse through the headers and collect role and session variables
for (header_name, header_value) in headers { for (header_name, header_value) in headers {
if let Ok(session_variable) = SessionVariable::from_str(header_name.as_str()) { if let Ok(session_variable) = SessionVariableName::from_str(header_name.as_str()) {
let variable_value = match header_value.to_str() { let variable_value = match header_value.to_str() {
Err(e) => Err(SessionError::InvalidHeaderValue { Err(e) => Err(SessionError::InvalidHeaderValue {
header_name: header_name.to_string(), header_name: header_name.to_string(),
@ -220,16 +220,16 @@ mod tests {
let mut authenticated_session_variables = HashMap::new(); let mut authenticated_session_variables = HashMap::new();
authenticated_session_variables.insert( authenticated_session_variables.insert(
SessionVariable::from_str("x-hasura-user-id").unwrap(), SessionVariableName::from_str("x-hasura-user-id").unwrap(),
SessionVariableValue::new("1"), SessionVariableValue::new("1"),
); );
client_session_variables.insert( client_session_variables.insert(
SessionVariable::from_str("x-hasura-custom").unwrap(), SessionVariableName::from_str("x-hasura-custom").unwrap(),
SessionVariableValue::new("test"), SessionVariableValue::new("test"),
); );
client_session_variables.insert( client_session_variables.insert(
SessionVariable::from_str("x-hasura-custom-claim").unwrap(), SessionVariableName::from_str("x-hasura-custom-claim").unwrap(),
SessionVariableValue::new("claim-value"), SessionVariableValue::new("claim-value"),
); );
let role_authorization = RoleAuthorization { let role_authorization = RoleAuthorization {
@ -243,7 +243,7 @@ mod tests {
let mut expected_session_variables = client_session_variables.clone(); let mut expected_session_variables = client_session_variables.clone();
expected_session_variables.insert( expected_session_variables.insert(
SessionVariable::from_str("x-hasura-user-id").unwrap(), SessionVariableName::from_str("x-hasura-user-id").unwrap(),
SessionVariableValue::new("1"), SessionVariableValue::new("1"),
); );
pa::assert_eq!( pa::assert_eq!(
@ -262,23 +262,23 @@ mod tests {
let mut authenticated_session_variables = HashMap::new(); let mut authenticated_session_variables = HashMap::new();
authenticated_session_variables.insert( authenticated_session_variables.insert(
SessionVariable::from_str("x-hasura-user-id").unwrap(), SessionVariableName::from_str("x-hasura-user-id").unwrap(),
SessionVariableValue::new("1"), SessionVariableValue::new("1"),
); );
client_session_variables.insert( client_session_variables.insert(
SessionVariable::from_str("x-hasura-custom").unwrap(), SessionVariableName::from_str("x-hasura-custom").unwrap(),
SessionVariableValue::new("test"), SessionVariableValue::new("test"),
); );
client_session_variables.insert( client_session_variables.insert(
SessionVariable::from_str("x-hasura-custom-claim").unwrap(), SessionVariableName::from_str("x-hasura-custom-claim").unwrap(),
SessionVariableValue::new("claim-value"), SessionVariableValue::new("claim-value"),
); );
let mut allowed_sesion_variables_from_request = HashSet::new(); let mut allowed_sesion_variables_from_request = HashSet::new();
allowed_sesion_variables_from_request allowed_sesion_variables_from_request
.insert(SessionVariable::from_str("x-hasura-custom").unwrap()); .insert(SessionVariableName::from_str("x-hasura-custom").unwrap());
allowed_sesion_variables_from_request allowed_sesion_variables_from_request
.insert(SessionVariable::from_str("x-hasura-custom-author-id").unwrap()); .insert(SessionVariableName::from_str("x-hasura-custom-author-id").unwrap());
let role_authorization = RoleAuthorization { let role_authorization = RoleAuthorization {
role: Role::new("test-role"), role: Role::new("test-role"),
session_variables: authenticated_session_variables, session_variables: authenticated_session_variables,
@ -291,10 +291,10 @@ mod tests {
let mut expected_session_variables = client_session_variables; let mut expected_session_variables = client_session_variables;
expected_session_variables expected_session_variables
.remove(&SessionVariable::from_str("x-hasura-custom-claim").unwrap()); .remove(&SessionVariableName::from_str("x-hasura-custom-claim").unwrap());
expected_session_variables.insert( expected_session_variables.insert(
SessionVariable::from_str("x-hasura-user-id").unwrap(), SessionVariableName::from_str("x-hasura-user-id").unwrap(),
SessionVariableValue::new("1"), SessionVariableValue::new("1"),
); );
pa::assert_eq!( pa::assert_eq!(
@ -313,16 +313,16 @@ mod tests {
let mut authenticated_session_variables = HashMap::new(); let mut authenticated_session_variables = HashMap::new();
authenticated_session_variables.insert( authenticated_session_variables.insert(
SessionVariable::from_str("x-hasura-user-id").unwrap(), SessionVariableName::from_str("x-hasura-user-id").unwrap(),
SessionVariableValue::new("1"), SessionVariableValue::new("1"),
); );
client_session_variables.insert( client_session_variables.insert(
SessionVariable::from_str("x-hasura-custom").unwrap(), SessionVariableName::from_str("x-hasura-custom").unwrap(),
SessionVariableValue::new("test"), SessionVariableValue::new("test"),
); );
client_session_variables.insert( client_session_variables.insert(
SessionVariable::from_str("x-hasura-custom-claim").unwrap(), SessionVariableName::from_str("x-hasura-custom-claim").unwrap(),
SessionVariableValue::new("claim-value"), SessionVariableValue::new("claim-value"),
); );
@ -337,7 +337,7 @@ mod tests {
let mut expected_session_variables = HashMap::new(); let mut expected_session_variables = HashMap::new();
expected_session_variables.insert( expected_session_variables.insert(
SessionVariable::from_str("x-hasura-user-id").unwrap(), SessionVariableName::from_str("x-hasura-user-id").unwrap(),
SessionVariableValue::new("1"), SessionVariableValue::new("1"),
); );

View File

@ -110,10 +110,11 @@ pub async fn authenticate_request(
mod tests { mod tests {
use std::str::FromStr; use std::str::FromStr;
use auth_base::{RoleAuthorization, SessionVariable, SessionVariableValue}; use auth_base::{RoleAuthorization, SessionVariableValue};
use jsonwebtoken as jwt; use jsonwebtoken as jwt;
use jsonwebtoken::Algorithm; use jsonwebtoken::Algorithm;
use jwt::{encode, EncodingKey}; use jwt::{encode, EncodingKey};
use open_dds::session_variables::SessionVariableName;
use reqwest::header::AUTHORIZATION; use reqwest::header::AUTHORIZATION;
use serde_json::json; use serde_json::json;
use tokio; use tokio;
@ -166,7 +167,7 @@ mod tests {
fn get_default_hasura_claims() -> HasuraClaims { fn get_default_hasura_claims() -> HasuraClaims {
let mut hasura_custom_claims = HashMap::new(); let mut hasura_custom_claims = HashMap::new();
hasura_custom_claims.insert( hasura_custom_claims.insert(
SessionVariable::from_str("x-hasura-user-id").unwrap(), SessionVariableName::from_str("x-hasura-user-id").unwrap(),
SessionVariableValue("1".to_string()), SessionVariableValue("1".to_string()),
); );
HasuraClaims { HasuraClaims {
@ -223,7 +224,7 @@ mod tests {
let mut role_authorization_session_variables = HashMap::new(); let mut role_authorization_session_variables = HashMap::new();
role_authorization_session_variables.insert( role_authorization_session_variables.insert(
SessionVariable::from_str("x-hasura-user-id").unwrap(), SessionVariableName::from_str("x-hasura-user-id").unwrap(),
SessionVariableValue::new("1"), SessionVariableValue::new("1"),
); );
expected_allowed_roles.insert( expected_allowed_roles.insert(
@ -272,7 +273,7 @@ mod tests {
async fn test_successful_role_emulation() -> anyhow::Result<()> { async fn test_successful_role_emulation() -> anyhow::Result<()> {
let mut hasura_claims = get_default_hasura_claims(); let mut hasura_claims = get_default_hasura_claims();
hasura_claims.custom_claims.insert( hasura_claims.custom_claims.insert(
SessionVariable::from_str("x-hasura-role").unwrap(), SessionVariableName::from_str("x-hasura-role").unwrap(),
SessionVariableValue::new("admin"), SessionVariableValue::new("admin"),
); );
let encoded_claims = get_encoded_claims(Algorithm::HS256, &hasura_claims)?; let encoded_claims = get_encoded_claims(Algorithm::HS256, &hasura_claims)?;

View File

@ -4,10 +4,11 @@ use std::time::Duration;
use axum::http::{HeaderMap, HeaderValue}; use axum::http::{HeaderMap, HeaderValue};
use axum::response::IntoResponse; use axum::response::IntoResponse;
use cookie::{self, Cookie}; use cookie::{self, Cookie};
use hasura_authn_core::{Role, SessionVariable, SessionVariableValue}; use hasura_authn_core::{Role, SessionVariableValue};
use jsonptr::Pointer; use jsonptr::Pointer;
use jsonwebtoken::{self as jwt, decode, DecodingKey, Validation}; use jsonwebtoken::{self as jwt, decode, DecodingKey, Validation};
use jwt::decode_header; use jwt::decode_header;
use open_dds::session_variables::SessionVariableName;
use reqwest::header::{AUTHORIZATION, COOKIE}; use reqwest::header::{AUTHORIZATION, COOKIE};
use reqwest::StatusCode; use reqwest::StatusCode;
use schemars::gen::SchemaGenerator; use schemars::gen::SchemaGenerator;
@ -253,7 +254,7 @@ pub struct JWTClaimsMap {
/// A dictionary of the custom claims, where the key is the name of the claim and the value /// A dictionary of the custom claims, where the key is the name of the claim and the value
/// is the JSON pointer to lookup the custom claims within the decoded JWT. /// is the JSON pointer to lookup the custom claims within the decoded JWT.
pub custom_claims: pub custom_claims:
Option<HashMap<SessionVariable, JWTClaimsMappingEntry<SessionVariableValue>>>, Option<HashMap<SessionVariableName, JWTClaimsMappingEntry<SessionVariableValue>>>,
} }
#[derive(Serialize, Deserialize, PartialEq, Clone, JsonSchema, Debug)] #[derive(Serialize, Deserialize, PartialEq, Clone, JsonSchema, Debug)]
@ -390,7 +391,7 @@ pub struct HasuraClaims {
/// as per the user's defined permissions. /// as per the user's defined permissions.
/// For example, things like `x-hasura-user-id` can go here. /// For example, things like `x-hasura-user-id` can go here.
#[serde(flatten)] #[serde(flatten)]
pub custom_claims: HashMap<SessionVariable, SessionVariableValue>, pub custom_claims: HashMap<SessionVariableName, SessionVariableValue>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -733,7 +734,7 @@ mod tests {
fn get_default_hasura_claims() -> HasuraClaims { fn get_default_hasura_claims() -> HasuraClaims {
let mut hasura_custom_claims = HashMap::new(); let mut hasura_custom_claims = HashMap::new();
hasura_custom_claims.insert( hasura_custom_claims.insert(
SessionVariable::from_str("x-hasura-user-id").unwrap(), SessionVariableName::from_str("x-hasura-user-id").unwrap(),
SessionVariableValue("1".to_string()), SessionVariableValue("1".to_string()),
); );
HasuraClaims { HasuraClaims {

View File

@ -1,4 +1,4 @@
use hasura_authn_core::{Role, SessionVariable, SessionVariableValue}; use hasura_authn_core::{Role, SessionVariableName, SessionVariableValue};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
@ -15,7 +15,7 @@ pub struct NoAuthConfig {
pub role: Role, pub role: Role,
/// static session variables to use whilst running the engine /// static session variables to use whilst running the engine
#[schemars(title = "SessionVariables")] #[schemars(title = "SessionVariables")]
pub session_variables: HashMap<SessionVariable, SessionVariableValue>, pub session_variables: HashMap<SessionVariableName, SessionVariableValue>,
} }
impl NoAuthConfig { impl NoAuthConfig {

View File

@ -3,7 +3,7 @@ use std::str::FromStr;
use std::sync::OnceLock; use std::sync::OnceLock;
use std::time::Duration; use std::time::Duration;
use auth_base::{Identity, Role, RoleAuthorization, SessionVariable, SessionVariableValue}; use auth_base::{Identity, Role, RoleAuthorization, SessionVariableName, SessionVariableValue};
use axum::{ use axum::{
http::{HeaderMap, HeaderName, StatusCode}, http::{HeaderMap, HeaderName, StatusCode},
response::IntoResponse, response::IntoResponse,
@ -206,7 +206,7 @@ async fn make_auth_hook_request(
response.json().await.map_err(InternalError::ReqwestError)?; response.json().await.map_err(InternalError::ReqwestError)?;
let mut session_variables = HashMap::new(); let mut session_variables = HashMap::new();
for (k, v) in &auth_hook_response { for (k, v) in &auth_hook_response {
match SessionVariable::from_str(k) { match SessionVariableName::from_str(k) {
Ok(session_variable) => { Ok(session_variable) => {
session_variables session_variables
.insert(session_variable, SessionVariableValue(v.to_string())); .insert(session_variable, SessionVariableValue(v.to_string()));
@ -366,11 +366,11 @@ mod tests {
let mut expected_allowed_roles = HashMap::new(); let mut expected_allowed_roles = HashMap::new();
let mut role_authorization_session_variables = HashMap::new(); let mut role_authorization_session_variables = HashMap::new();
role_authorization_session_variables.insert( role_authorization_session_variables.insert(
SessionVariable::from_str("x-hasura-role").unwrap(), SessionVariableName::from_str("x-hasura-role").unwrap(),
SessionVariableValue::new("test-role"), SessionVariableValue::new("test-role"),
); );
role_authorization_session_variables.insert( role_authorization_session_variables.insert(
SessionVariable::from_str("x-hasura-test-role-id").unwrap(), SessionVariableName::from_str("x-hasura-test-role-id").unwrap(),
SessionVariableValue::new("1"), SessionVariableValue::new("1"),
); );
expected_allowed_roles.insert( expected_allowed_roles.insert(
@ -437,11 +437,11 @@ mod tests {
let mut expected_allowed_roles = HashMap::new(); let mut expected_allowed_roles = HashMap::new();
let mut role_authorization_session_variables = HashMap::new(); let mut role_authorization_session_variables = HashMap::new();
role_authorization_session_variables.insert( role_authorization_session_variables.insert(
SessionVariable::from_str("x-hasura-role").unwrap(), SessionVariableName::from_str("x-hasura-role").unwrap(),
SessionVariableValue::new("test-role"), SessionVariableValue::new("test-role"),
); );
role_authorization_session_variables.insert( role_authorization_session_variables.insert(
SessionVariable::from_str("x-hasura-test-role-id").unwrap(), SessionVariableName::from_str("x-hasura-test-role-id").unwrap(),
SessionVariableValue::new("1"), SessionVariableValue::new("1"),
); );
expected_allowed_roles.insert( expected_allowed_roles.insert(
@ -509,15 +509,15 @@ mod tests {
let mut expected_allowed_roles = HashMap::new(); let mut expected_allowed_roles = HashMap::new();
let mut role_authorization_session_variables = HashMap::new(); let mut role_authorization_session_variables = HashMap::new();
role_authorization_session_variables.insert( role_authorization_session_variables.insert(
SessionVariable::from_str("x-hasura-role").unwrap(), SessionVariableName::from_str("x-hasura-role").unwrap(),
SessionVariableValue::new("test-role"), SessionVariableValue::new("test-role"),
); );
role_authorization_session_variables.insert( role_authorization_session_variables.insert(
SessionVariable::from_str("x-hasura-test-role-id").unwrap(), SessionVariableName::from_str("x-hasura-test-role-id").unwrap(),
SessionVariableValue::new("1"), SessionVariableValue::new("1"),
); );
role_authorization_session_variables.insert( role_authorization_session_variables.insert(
SessionVariable::from_str("status").unwrap(), SessionVariableName::from_str("status").unwrap(),
SessionVariableValue::new("true"), SessionVariableValue::new("true"),
); );
expected_allowed_roles.insert( expected_allowed_roles.insert(
@ -637,11 +637,11 @@ mod tests {
let mut expected_allowed_roles = HashMap::new(); let mut expected_allowed_roles = HashMap::new();
let mut role_authorization_session_variables = HashMap::new(); let mut role_authorization_session_variables = HashMap::new();
role_authorization_session_variables.insert( role_authorization_session_variables.insert(
SessionVariable::from_str("x-hasura-role").unwrap(), SessionVariableName::from_str("x-hasura-role").unwrap(),
SessionVariableValue::new("user"), SessionVariableValue::new("user"),
); );
role_authorization_session_variables.insert( role_authorization_session_variables.insert(
SessionVariable::from_str("x-hasura-user-id").unwrap(), SessionVariableName::from_str("x-hasura-user-id").unwrap(),
SessionVariableValue::new("1"), SessionVariableValue::new("1"),
); );
expected_allowed_roles.insert( expected_allowed_roles.insert(

View File

@ -7,7 +7,7 @@ use hasura_authn_core::{Identity, Role, Session, SessionError, SessionVariableVa
use lang_graphql::ast::common as ast; use lang_graphql::ast::common as ast;
use lang_graphql::{http::RawRequest, schema::Schema}; use lang_graphql::{http::RawRequest, schema::Schema};
use metadata_resolve::{data_connectors::NdcVersion, LifecyclePluginConfigs}; use metadata_resolve::{data_connectors::NdcVersion, LifecyclePluginConfigs};
use open_dds::session_variables::{SessionVariable, SESSION_VARIABLE_ROLE}; use open_dds::session_variables::{SessionVariableName, SESSION_VARIABLE_ROLE};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use serde_json as json; use serde_json as json;
use sql::execute::SqlRequest; use sql::execute::SqlRequest;
@ -41,7 +41,7 @@ pub fn setup(test_dir: &Path) -> GoldenTestContext {
} }
pub(crate) fn resolve_session( pub(crate) fn resolve_session(
session_variables: HashMap<SessionVariable, SessionVariableValue>, session_variables: HashMap<SessionVariableName, SessionVariableValue>,
) -> Result<Session, SessionError> { ) -> Result<Session, SessionError> {
//return an arbitrary identity with role emulation enabled //return an arbitrary identity with role emulation enabled
let authorization = Identity::admin(Role::new("admin")); let authorization = Identity::admin(Role::new("admin"));
@ -107,7 +107,7 @@ pub(crate) fn test_introspection_expectation(
let request_headers = reqwest::header::HeaderMap::new(); let request_headers = reqwest::header::HeaderMap::new();
let session_vars_path = &test_path.join("session_variables.json"); let session_vars_path = &test_path.join("session_variables.json");
let sessions: Vec<HashMap<SessionVariable, SessionVariableValue>> = let sessions: Vec<HashMap<SessionVariableName, SessionVariableValue>> =
json::from_str(read_to_string(session_vars_path)?.as_ref())?; json::from_str(read_to_string(session_vars_path)?.as_ref())?;
let sessions: Vec<Session> = sessions let sessions: Vec<Session> = sessions
.into_iter() .into_iter()
@ -266,7 +266,7 @@ pub fn test_execution_expectation_for_multiple_ndc_versions(
let request_headers = reqwest::header::HeaderMap::new(); let request_headers = reqwest::header::HeaderMap::new();
let session_vars_path = &test_path.join("session_variables.json"); let session_vars_path = &test_path.join("session_variables.json");
let sessions: Vec<HashMap<SessionVariable, SessionVariableValue>> = let sessions: Vec<HashMap<SessionVariableName, SessionVariableValue>> =
json::from_str(read_to_string(session_vars_path)?.as_ref())?; json::from_str(read_to_string(session_vars_path)?.as_ref())?;
let sessions: Vec<Session> = sessions let sessions: Vec<Session> = sessions
.into_iter() .into_iter()
@ -472,7 +472,7 @@ pub fn test_execute_explain(
let session_variables_raw = r#"{ let session_variables_raw = r#"{
"x-hasura-role": "admin" "x-hasura-role": "admin"
}"#; }"#;
let session_variables: HashMap<SessionVariable, SessionVariableValue> = let session_variables: HashMap<SessionVariableName, SessionVariableValue> =
serde_json::from_str(session_variables_raw)?; serde_json::from_str(session_variables_raw)?;
resolve_session(session_variables) resolve_session(session_variables)
}?; }?;
@ -579,7 +579,7 @@ pub(crate) fn test_sql(test_path_string: &str) -> anyhow::Result<()> {
let session = Arc::new({ let session = Arc::new({
let session_vars_path = &test_path.join("session_variables.json"); let session_vars_path = &test_path.join("session_variables.json");
let session_variables: HashMap<SessionVariable, SessionVariableValue> = let session_variables: HashMap<SessionVariableName, SessionVariableValue> =
serde_json::from_str(read_to_string(session_vars_path)?.as_ref())?; serde_json::from_str(read_to_string(session_vars_path)?.as_ref())?;
resolve_session(session_variables) resolve_session(session_variables)
}?); }?);

View File

@ -0,0 +1,11 @@
[
{
"data": {
"AuthorMany": [
{
"author_id": 1
}
]
}
}
]

View File

@ -0,0 +1,192 @@
{
"version": "v2",
"flags": {
"json_session_variables": true
},
"subgraphs": [
{
"name": "default",
"objects": [
{
"kind": "BooleanExpressionType",
"version": "v1",
"definition": {
"name": "int_bool_exp",
"operand": {
"scalar": {
"type": "Int",
"comparisonOperators": [
{
"name": "_eq",
"argumentType": "Int!"
},
{
"name": "_in",
"argumentType": "[Int!]"
}
],
"dataConnectorOperatorMapping": [
{
"dataConnectorName": "db",
"dataConnectorScalarType": "int4",
"operatorMapping": {}
}
]
}
},
"logicalOperators": {
"enable": true
},
"isNull": {
"enable": true
},
"graphql": {
"typeName": "Int_Filter"
}
}
},
{
"kind": "ObjectType",
"version": "v1",
"definition": {
"name": "author",
"fields": [
{
"name": "author_id",
"type": "Int!"
},
{
"name": "first_name",
"type": "String!"
},
{
"name": "last_name",
"type": "String!"
}
],
"graphql": {
"typeName": "Author"
},
"dataConnectorTypeMapping": [
{
"dataConnectorName": "db",
"dataConnectorObjectType": "author",
"fieldMapping": {
"author_id": {
"column": {
"name": "id"
}
},
"first_name": {
"column": {
"name": "first_name"
}
},
"last_name": {
"column": {
"name": "last_name"
}
}
}
}
]
}
},
{
"kind": "BooleanExpressionType",
"version": "v1",
"definition": {
"name": "author_bool_exp",
"operand": {
"object": {
"type": "author",
"comparableFields": [
{
"fieldName": "author_id",
"booleanExpressionType": "int_bool_exp"
}
],
"comparableRelationships": []
}
},
"logicalOperators": {
"enable": true
},
"isNull": {
"enable": true
},
"graphql": {
"typeName": "Author_Filter"
}
}
},
{
"kind": "Model",
"version": "v1",
"definition": {
"name": "Authors",
"objectType": "author",
"source": {
"dataConnectorName": "db",
"collection": "author"
},
"graphql": {
"selectUniques": [],
"selectMany": {
"queryRootField": "AuthorMany"
},
"orderByExpressionType": "Author_Order_By"
},
"filterExpressionType": "author_bool_exp",
"orderableFields": [
{
"fieldName": "author_id",
"orderByDirections": {
"enableAll": true
}
}
]
}
},
{
"kind": "TypePermissions",
"version": "v1",
"definition": {
"typeName": "author",
"permissions": [
{
"role": "user",
"output": {
"allowedFields": ["author_id", "first_name", "last_name"]
}
}
]
}
},
{
"kind": "ModelPermissions",
"version": "v1",
"definition": {
"modelName": "Authors",
"permissions": [
{
"role": "user",
"select": {
"filter": {
"fieldComparison": {
"field": "author_id",
"operator": "_in",
"value": {
"sessionVariable": "x-hasura-allowed-author-ids"
}
}
}
}
}
]
}
}
]
}
]
}

View File

@ -0,0 +1,5 @@
query MyQuery {
AuthorMany {
author_id
}
}

View File

@ -0,0 +1,6 @@
[
{
"x-hasura-role": "user",
"x-hasura-allowed-author-ids": "[1]"
}
]

View File

@ -129,6 +129,17 @@ fn test_model_select_many_empty_select() -> anyhow::Result<()> {
) )
} }
#[test]
fn test_model_select_many_array_session_variable() -> anyhow::Result<()> {
let test_path_string = "execute/models/select_many/array_session_variable";
let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json";
common::test_execution_expectation(
test_path_string,
&[common_metadata_path_string],
common::TestOpenDDPipeline::TestNDCResponses,
)
}
#[test] #[test]
fn test_model_select_many_field_arguments() -> anyhow::Result<()> { fn test_model_select_many_field_arguments() -> anyhow::Result<()> {
common::test_execution_expectation_for_multiple_ndc_versions( common::test_execution_expectation_for_multiple_ndc_versions(

View File

@ -0,0 +1,52 @@
---
source: crates/engine/tests/common.rs
expression: query_ir
---
V1(
QueryRequestV1 {
queries: {
Alias(
Identifier(
"AuthorMany",
),
): Model(
ModelSelection {
target: ModelTarget {
subgraph: SubgraphName(
"default",
),
model_name: ModelName(
Identifier(
"Authors",
),
),
arguments: {},
filter: None,
order_by: [],
limit: None,
offset: None,
},
selection: {
Alias(
Identifier(
"author_id",
),
): Field(
ObjectFieldSelection {
target: ObjectFieldTarget {
field_name: FieldName(
Identifier(
"author_id",
),
),
arguments: {},
},
selection: None,
},
),
},
},
),
},
},
)

View File

@ -0,0 +1,52 @@
---
source: crates/engine/tests/common.rs
expression: query_ir
---
V1(
QueryRequestV1 {
queries: {
Alias(
Identifier(
"AuthorMany",
),
): Model(
ModelSelection {
target: ModelTarget {
subgraph: SubgraphName(
"default",
),
model_name: ModelName(
Identifier(
"Authors",
),
),
arguments: {},
filter: None,
order_by: [],
limit: None,
offset: None,
},
selection: {
Alias(
Identifier(
"author_id",
),
): Field(
ObjectFieldSelection {
target: ObjectFieldTarget {
field_name: FieldName(
Identifier(
"author_id",
),
),
arguments: {},
},
selection: None,
},
),
},
},
),
},
},
)

View File

@ -0,0 +1,13 @@
---
source: crates/engine/tests/common.rs
expression: rowsets
---
[
{
"rows": [
{
"author_id": 1
}
]
}
]

View File

@ -0,0 +1,15 @@
---
source: crates/engine/tests/common.rs
expression: rowsets
---
{
"Ok": [
{
"rows": [
{
"author_id": 1
}
]
}
]
}

View File

@ -21,7 +21,7 @@ mod tests {
use hasura_authn_core::{Identity, Role, Session, SessionVariableValue}; use hasura_authn_core::{Identity, Role, Session, SessionVariableValue};
use lang_graphql::http::Request; use lang_graphql::http::Request;
use lang_graphql::{parser::Parser, validation::normalize_request}; use lang_graphql::{parser::Parser, validation::normalize_request};
use open_dds::session_variables::{SessionVariable, SESSION_VARIABLE_ROLE}; use open_dds::session_variables::{SessionVariableName, SESSION_VARIABLE_ROLE};
use serde_json as json; use serde_json as json;
use std::{ use std::{
collections::HashMap, collections::HashMap,
@ -160,7 +160,7 @@ mod tests {
// TODO: remove duplication between this function and 'add_session' // TODO: remove duplication between this function and 'add_session'
fn resolve_session(session_vars_path: PathBuf) -> Session { fn resolve_session(session_vars_path: PathBuf) -> Session {
let authorization = Identity::admin(Role::new("admin")); let authorization = Identity::admin(Role::new("admin"));
let session_variables: HashMap<SessionVariable, SessionVariableValue> = { let session_variables: HashMap<SessionVariableName, SessionVariableValue> = {
if session_vars_path.exists() { if session_vars_path.exists() {
json::from_str(fs::read_to_string(session_vars_path).unwrap().as_ref()).unwrap() json::from_str(fs::read_to_string(session_vars_path).unwrap().as_ref()).unwrap()
} else { } else {

View File

@ -369,8 +369,8 @@ pub(crate) fn map_argument_value_to_ndc_type(
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use hasura_authn_core::{ use hasura_authn_core::{
Role, RoleAuthorization, Session, SessionVariable, SessionVariableList, Role, RoleAuthorization, Session, SessionVariableList, SessionVariableName,
SessionVariableValue, SessionVariableReference, SessionVariableValue,
}; };
use indexmap::IndexMap; use indexmap::IndexMap;
use metadata_resolve::http::SerializableHeaderName; use metadata_resolve::http::SerializableHeaderName;
@ -380,7 +380,7 @@ mod test {
use std::str::FromStr; use std::str::FromStr;
fn make_test_session( fn make_test_session(
client_session_variables: HashMap<SessionVariable, SessionVariableValue>, client_session_variables: HashMap<SessionVariableName, SessionVariableValue>,
) -> Session { ) -> Session {
let authenticated_session_variables = HashMap::new(); let authenticated_session_variables = HashMap::new();
@ -459,7 +459,10 @@ mod test {
let mut additional = IndexMap::new(); let mut additional = IndexMap::new();
additional.insert( additional.insert(
SerializableHeaderName::new("name".into()).unwrap(), SerializableHeaderName::new("name".into()).unwrap(),
ValueExpression::SessionVariable(SessionVariable::from_str("x-name").unwrap()), ValueExpression::SessionVariable(SessionVariableReference {
name: SessionVariableName::from_str("x-name").unwrap(),
passed_as_json: false,
}),
); );
let http_headers = HttpHeadersPreset { let http_headers = HttpHeadersPreset {
forward: vec![], forward: vec![],
@ -471,7 +474,7 @@ mod test {
// what session variables do we have? // what session variables do we have?
let mut client_session_variables = HashMap::new(); let mut client_session_variables = HashMap::new();
client_session_variables.insert( client_session_variables.insert(
SessionVariable::from_str("x-name").unwrap(), SessionVariableName::from_str("x-name").unwrap(),
SessionVariableValue::new("Mr Horse"), SessionVariableValue::new("Mr Horse"),
); );

View File

@ -5,7 +5,7 @@ use open_dds::{
arguments::ArgumentName, arguments::ArgumentName,
data_connector::{DataConnectorColumnName, DataConnectorName}, data_connector::{DataConnectorColumnName, DataConnectorName},
relationships::RelationshipName, relationships::RelationshipName,
session_variables::SessionVariable, session_variables::SessionVariableName,
types::{CustomTypeName, FieldName}, types::{CustomTypeName, FieldName},
}; };
use serde_json as json; use serde_json as json;
@ -118,7 +118,9 @@ pub enum InternalDeveloperError {
}, },
#[error("Required session variable not found in the request: {session_variable}")] #[error("Required session variable not found in the request: {session_variable}")]
MissingSessionVariable { session_variable: SessionVariable }, MissingSessionVariable {
session_variable: SessionVariableName,
},
#[error("Unable to typecast session variable. Expected: {expected:}, but found: {found:}")] #[error("Unable to typecast session variable. Expected: {expected:}, but found: {found:}")]
VariableTypeCast { expected: String, found: String }, VariableTypeCast { expected: String, found: String },

View File

@ -223,13 +223,13 @@ pub(crate) fn make_argument_from_value_expression(
match val_expr { match val_expr {
metadata_resolve::ValueExpression::Literal(val) => Ok(val.clone()), metadata_resolve::ValueExpression::Literal(val) => Ok(val.clone()),
metadata_resolve::ValueExpression::SessionVariable(session_var) => { metadata_resolve::ValueExpression::SessionVariable(session_var) => {
let value = session_variables.get(session_var).ok_or_else(|| { let value = session_variables.get(&session_var.name).ok_or_else(|| {
error::InternalDeveloperError::MissingSessionVariable { error::InternalDeveloperError::MissingSessionVariable {
session_variable: session_var.clone(), session_variable: session_var.name.clone(),
} }
})?; })?;
typecast_session_variable(value, value_type) typecast_session_variable(session_var.passed_as_json, value, value_type)
} }
} }
} }
@ -247,14 +247,14 @@ pub(crate) fn make_argument_from_value_expression_or_predicate<'s>(
Ok(Argument::Literal { value: val.clone() }) Ok(Argument::Literal { value: val.clone() })
} }
metadata_resolve::ValueExpressionOrPredicate::SessionVariable(session_var) => { metadata_resolve::ValueExpressionOrPredicate::SessionVariable(session_var) => {
let value = session_variables.get(session_var).ok_or_else(|| { let value = session_variables.get(&session_var.name).ok_or_else(|| {
error::InternalDeveloperError::MissingSessionVariable { error::InternalDeveloperError::MissingSessionVariable {
session_variable: session_var.clone(), session_variable: session_var.name.clone(),
} }
})?; })?;
Ok(Argument::Literal { Ok(Argument::Literal {
value: typecast_session_variable(value, value_type)?, value: typecast_session_variable(session_var.passed_as_json, value, value_type)?,
}) })
} }
metadata_resolve::ValueExpressionOrPredicate::BooleanExpression(model_predicate) => { metadata_resolve::ValueExpressionOrPredicate::BooleanExpression(model_predicate) => {
@ -274,6 +274,18 @@ pub(crate) fn make_argument_from_value_expression_or_predicate<'s>(
/// Typecast a stringified session variable into a given type, but as a serde_json::Value /// Typecast a stringified session variable into a given type, but as a serde_json::Value
fn typecast_session_variable( fn typecast_session_variable(
passed_as_json: bool,
session_var_value_wrapped: &SessionVariableValue,
to_type: &QualifiedTypeReference,
) -> Result<serde_json::Value, error::Error> {
if passed_as_json {
typecast_session_variable_v2(session_var_value_wrapped, to_type)
} else {
typecast_session_variable_v1(session_var_value_wrapped, to_type)
}
}
fn typecast_session_variable_v1(
session_var_value_wrapped: &SessionVariableValue, session_var_value_wrapped: &SessionVariableValue,
to_type: &QualifiedTypeReference, to_type: &QualifiedTypeReference,
) -> Result<serde_json::Value, error::Error> { ) -> Result<serde_json::Value, error::Error> {
@ -328,3 +340,73 @@ fn typecast_session_variable(
QualifiedBaseType::List(_) => Err(error::InternalDeveloperError::VariableArrayTypeCast)?, QualifiedBaseType::List(_) => Err(error::InternalDeveloperError::VariableArrayTypeCast)?,
} }
} }
fn typecast_session_variable_v2(
session_var_value: &SessionVariableValue,
to_type: &QualifiedTypeReference,
) -> Result<serde_json::Value, error::Error> {
let value = serde_json::from_str(&session_var_value.0)?;
typecheck_session_variable(&value, to_type)?;
Ok(value)
}
fn typecheck_session_variable(
value: &serde_json::Value,
to_type: &QualifiedTypeReference,
) -> Result<(), crate::Error> {
match &to_type.underlying_type {
QualifiedBaseType::Named(type_name) => match type_name {
QualifiedTypeName::Inbuilt(primitive) => match primitive {
InbuiltType::Int => {
if !value.is_i64() {
Err(error::InternalDeveloperError::VariableTypeCast {
expected: "int".into(),
found: value.to_string(),
})?;
}
Ok(())
}
InbuiltType::Float => {
if !value.is_f64() {
Err(error::InternalDeveloperError::VariableTypeCast {
expected: "float".into(),
found: value.to_string(),
})?;
}
Ok(())
}
InbuiltType::Boolean => {
if !value.is_boolean() {
Err(error::InternalDeveloperError::VariableTypeCast {
expected: "true or false".into(),
found: value.to_string(),
})?;
}
Ok(())
}
InbuiltType::ID | InbuiltType::String => {
if !value.is_string() {
Err(error::InternalDeveloperError::VariableTypeCast {
expected: "string".into(),
found: value.to_string(),
})?;
}
Ok(())
}
},
QualifiedTypeName::Custom(_) => Ok(()),
},
QualifiedBaseType::List(element_type) => {
let elements = value.as_array().ok_or_else(|| {
error::InternalDeveloperError::VariableTypeCast {
expected: "array".into(),
found: value.to_string(),
}
})?;
for element in elements {
typecheck_session_variable(element, element_type)?;
}
Ok(())
}
}
}

View File

@ -253,6 +253,7 @@ pub fn get_argument_mappings<'a>(
/// type to validate it against to ensure the fields it refers to /// type to validate it against to ensure the fields it refers to
/// exist etc /// exist etc
pub(crate) fn resolve_value_expression_for_argument( pub(crate) fn resolve_value_expression_for_argument(
flags: &open_dds::flags::Flags,
argument_name: &open_dds::arguments::ArgumentName, argument_name: &open_dds::arguments::ArgumentName,
value_expression: &open_dds::permissions::ValueExpressionOrPredicate, value_expression: &open_dds::permissions::ValueExpressionOrPredicate,
argument_type: &QualifiedTypeReference, argument_type: &QualifiedTypeReference,
@ -278,7 +279,10 @@ pub(crate) fn resolve_value_expression_for_argument(
match value_expression { match value_expression {
open_dds::permissions::ValueExpressionOrPredicate::SessionVariable(session_variable) => { open_dds::permissions::ValueExpressionOrPredicate::SessionVariable(session_variable) => {
Ok::<ValueExpressionOrPredicate, Error>(ValueExpressionOrPredicate::SessionVariable( Ok::<ValueExpressionOrPredicate, Error>(ValueExpressionOrPredicate::SessionVariable(
session_variable.clone(), hasura_authn_core::SessionVariableReference {
name: session_variable.clone(),
passed_as_json: flags.json_session_variables,
},
)) ))
} }
open_dds::permissions::ValueExpressionOrPredicate::Literal(json_value) => { open_dds::permissions::ValueExpressionOrPredicate::Literal(json_value) => {
@ -365,6 +369,7 @@ pub(crate) fn resolve_value_expression_for_argument(
})?; })?;
let resolved_model_predicate = model_permissions::resolve_model_predicate_with_type( let resolved_model_predicate = model_permissions::resolve_model_predicate_with_type(
flags,
bool_exp, bool_exp,
base_type, base_type,
object_type_representation, object_type_representation,

View File

@ -304,17 +304,7 @@ fn build_preset_map_from_input_object_type_permission(
field_path: new_field_path, field_path: new_field_path,
}; };
let value = ( let value = (type_reference.clone(), preset.value.clone());
type_reference.clone(),
match &preset.value {
open_dds::permissions::ValueExpression::Literal(literal) => {
ValueExpressionOrPredicate::Literal(literal.clone())
}
open_dds::permissions::ValueExpression::SessionVariable(session_variable) => {
ValueExpressionOrPredicate::SessionVariable(session_variable.clone())
}
},
);
Ok((key, value)) Ok((key, value))
}) })

View File

@ -38,6 +38,7 @@ fn get_command_source_argument<'a>(
} }
pub fn resolve_command_permissions( pub fn resolve_command_permissions(
flags: &open_dds::flags::Flags,
command: &commands::Command, command: &commands::Command,
permissions: &CommandPermissionsV1, permissions: &CommandPermissionsV1,
object_types: &BTreeMap< object_types: &BTreeMap<
@ -97,6 +98,7 @@ pub fn resolve_command_permissions(
match command.arguments.get(&argument_preset.argument) { match command.arguments.get(&argument_preset.argument) {
Some(argument) => { Some(argument) => {
let value_expression = resolve_value_expression_for_argument( let value_expression = resolve_value_expression_for_argument(
flags,
&argument_preset.argument, &argument_preset.argument,
&argument_preset.value, &argument_preset.value,
&argument.argument_type, &argument.argument_type,

View File

@ -67,6 +67,7 @@ pub fn resolve(
})?; })?;
if command.permissions.is_empty() { if command.permissions.is_empty() {
command.permissions = command_permission::resolve_command_permissions( command.permissions = command_permission::resolve_command_permissions(
&metadata_accessor.flags,
&command.command, &command.command,
command_permissions, command_permissions,
object_types, object_types,

View File

@ -29,12 +29,15 @@ pub fn resolve<'a>(
let qualified_data_connector_name = let qualified_data_connector_name =
Qualified::new(subgraph.clone(), data_connector.name.clone()); Qualified::new(subgraph.clone(), data_connector.name.clone());
let (data_connector_context, connector_issues) = let (data_connector_context, connector_issues) = types::DataConnectorContext::new(
types::DataConnectorContext::new(data_connector, &configuration.unstable_features) metadata_accessor,
.map_err(|error| NamedDataConnectorError { data_connector,
data_connector_name: qualified_data_connector_name.clone(), &configuration.unstable_features,
error, )
})?; .map_err(|error| NamedDataConnectorError {
data_connector_name: qualified_data_connector_name.clone(),
error,
})?;
issues.extend( issues.extend(
connector_issues connector_issues

View File

@ -12,6 +12,7 @@ use crate::types::subgraph::Qualified;
use indexmap::IndexMap; use indexmap::IndexMap;
use lang_graphql::ast::common::OperationType; use lang_graphql::ast::common::OperationType;
use ndc_models; use ndc_models;
use open_dds::accessor::MetadataAccessor;
use open_dds::data_connector::DataConnectorColumnName; use open_dds::data_connector::DataConnectorColumnName;
use open_dds::types::DataConnectorArgumentName; use open_dds::types::DataConnectorArgumentName;
use open_dds::{ use open_dds::{
@ -63,6 +64,7 @@ pub struct DataConnectorContext<'a> {
impl<'a> DataConnectorContext<'a> { impl<'a> DataConnectorContext<'a> {
pub fn new( pub fn new(
metadata_accessor: &MetadataAccessor,
data_connector: &'a data_connector::DataConnectorLinkV1, data_connector: &'a data_connector::DataConnectorLinkV1,
unstable_features: &UnstableFeatures, unstable_features: &UnstableFeatures,
) -> Result<(Self, Vec<DataConnectorIssue>), DataConnectorError> { ) -> Result<(Self, Vec<DataConnectorIssue>), DataConnectorError> {
@ -102,7 +104,8 @@ impl<'a> DataConnectorContext<'a> {
.argument_presets .argument_presets
.iter() .iter()
.map(|argument_preset| -> Result<_, DataConnectorError> { .map(|argument_preset| -> Result<_, DataConnectorError> {
let header_presets = HttpHeadersPreset::new(&argument_preset.value.http_headers)?; let header_presets =
HttpHeadersPreset::new(metadata_accessor, &argument_preset.value.http_headers)?;
Ok(ArgumentPreset { Ok(ArgumentPreset {
name: argument_preset.argument.clone(), name: argument_preset.argument.clone(),
value: ArgumentPresetValue { value: ArgumentPresetValue {
@ -376,6 +379,7 @@ pub struct HttpHeadersPreset {
impl HttpHeadersPreset { impl HttpHeadersPreset {
fn new( fn new(
metadata_accessor: &MetadataAccessor,
headers_preset: &open_dds::data_connector::HttpHeadersPreset, headers_preset: &open_dds::data_connector::HttpHeadersPreset,
) -> Result<Self, DataConnectorError> { ) -> Result<Self, DataConnectorError> {
let forward = headers_preset let forward = headers_preset
@ -389,7 +393,7 @@ impl HttpHeadersPreset {
.iter() .iter()
.map(|(header_name, header_val)| { .map(|(header_name, header_val)| {
let key = SerializableHeaderName::new(header_name.to_string()).map_err(to_error)?; let key = SerializableHeaderName::new(header_name.to_string()).map_err(to_error)?;
let val = resolve_value_expression(header_val.clone()); let val = resolve_value_expression(metadata_accessor, header_val.clone());
Ok((key, val)) Ok((key, val))
}) })
.collect::<Result<IndexMap<_, _>, DataConnectorError>>()?; .collect::<Result<IndexMap<_, _>, DataConnectorError>>()?;
@ -402,11 +406,15 @@ impl HttpHeadersPreset {
} }
fn resolve_value_expression( fn resolve_value_expression(
metadata_accessor: &MetadataAccessor,
value_expression_input: open_dds::permissions::ValueExpression, value_expression_input: open_dds::permissions::ValueExpression,
) -> ValueExpression { ) -> ValueExpression {
match value_expression_input { match value_expression_input {
open_dds::permissions::ValueExpression::SessionVariable(session_variable) => { open_dds::permissions::ValueExpression::SessionVariable(session_variable) => {
ValueExpression::SessionVariable(session_variable) ValueExpression::SessionVariable(hasura_authn_core::SessionVariableReference {
name: session_variable,
passed_as_json: metadata_accessor.flags.json_session_variables,
})
} }
open_dds::permissions::ValueExpression::Literal(json_value) => { open_dds::permissions::ValueExpression::Literal(json_value) => {
ValueExpression::Literal(json_value) ValueExpression::Literal(json_value)
@ -465,7 +473,7 @@ pub struct DataConnectorCapabilities {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use ndc_models; use ndc_models;
use open_dds::data_connector::DataConnectorLinkV1; use open_dds::{accessor::MetadataAccessor, data_connector::DataConnectorLinkV1, Metadata};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::{ use crate::{
@ -525,9 +533,13 @@ mod tests {
enable_ndc_v02_support: true, enable_ndc_v02_support: true,
..Default::default() ..Default::default()
}; };
let (context, issues) = let metadata_accessor = MetadataAccessor::new(Metadata::WithoutNamespaces(vec![]));
DataConnectorContext::new(&data_connector_with_capabilities, &unstable_features) let (context, issues) = DataConnectorContext::new(
.unwrap(); &metadata_accessor,
&data_connector_with_capabilities,
&unstable_features,
)
.unwrap();
assert_eq!(context.capabilities, explicit_capabilities); assert_eq!(context.capabilities, explicit_capabilities);
assert_eq!(context.supported_ndc_version, NdcVersion::V01); assert_eq!(context.supported_ndc_version, NdcVersion::V01);
assert_eq!(issues.len(), 0, "Issues: {issues:#?}"); assert_eq!(issues.len(), 0, "Issues: {issues:#?}");
@ -566,9 +578,13 @@ mod tests {
enable_ndc_v02_support: true, enable_ndc_v02_support: true,
..Default::default() ..Default::default()
}; };
let (context, issues) = let metadata_accessor = MetadataAccessor::new(Metadata::WithoutNamespaces(vec![]));
DataConnectorContext::new(&data_connector_with_capabilities, &unstable_features) let (context, issues) = DataConnectorContext::new(
.unwrap(); &metadata_accessor,
&data_connector_with_capabilities,
&unstable_features,
)
.unwrap();
assert_eq!(context.capabilities, explicit_capabilities); assert_eq!(context.capabilities, explicit_capabilities);
assert_eq!(context.supported_ndc_version, NdcVersion::V02); assert_eq!(context.supported_ndc_version, NdcVersion::V02);
assert_eq!(issues.len(), 0, "Issues: {issues:#?}"); assert_eq!(issues.len(), 0, "Issues: {issues:#?}");

View File

@ -81,6 +81,7 @@ pub fn resolve(
.and_then(|bool_exp| bool_exp.graphql.as_ref()); .and_then(|bool_exp| bool_exp.graphql.as_ref());
let select_permissions = model_permission::resolve_model_select_permissions( let select_permissions = model_permission::resolve_model_select_permissions(
&metadata_accessor.flags,
&model.model, &model.model,
subgraph, subgraph,
permissions, permissions,

View File

@ -33,6 +33,7 @@ use ref_cast::RefCast;
use std::collections::BTreeMap; use std::collections::BTreeMap;
fn resolve_model_predicate_with_model( fn resolve_model_predicate_with_model(
flags: &open_dds::flags::Flags,
model_predicate: &open_dds::permissions::ModelPredicate, model_predicate: &open_dds::permissions::ModelPredicate,
model: &models::Model, model: &models::Model,
subgraph: &SubgraphName, subgraph: &SubgraphName,
@ -102,6 +103,7 @@ fn resolve_model_predicate_with_model(
)?; )?;
resolve_model_predicate_with_type( resolve_model_predicate_with_type(
flags,
model_predicate, model_predicate,
&model.data_type, &model.data_type,
object_type_representation, object_type_representation,
@ -136,6 +138,7 @@ pub fn get_model_source_argument<'a>(
} }
pub fn resolve_model_select_permissions( pub fn resolve_model_select_permissions(
flags: &open_dds::flags::Flags,
model: &models::Model, model: &models::Model,
subgraph: &SubgraphName, subgraph: &SubgraphName,
model_permissions: &ModelPermissionsV1, model_permissions: &ModelPermissionsV1,
@ -163,6 +166,7 @@ pub fn resolve_model_select_permissions(
let resolved_predicate = match &select.filter { let resolved_predicate = match &select.filter {
NullableModelPredicate::NotNull(model_predicate) => { NullableModelPredicate::NotNull(model_predicate) => {
resolve_model_predicate_with_model( resolve_model_predicate_with_model(
flags,
model_predicate, model_predicate,
model, model,
subgraph, subgraph,
@ -210,6 +214,7 @@ pub fn resolve_model_select_permissions(
match model.arguments.get(&argument_preset.argument) { match model.arguments.get(&argument_preset.argument) {
Some(argument) => { Some(argument) => {
let value_expression = resolve_value_expression_for_argument( let value_expression = resolve_value_expression_for_argument(
flags,
&argument_preset.argument, &argument_preset.argument,
&argument_preset.value, &argument_preset.value,
&argument.argument_type, &argument.argument_type,
@ -269,6 +274,7 @@ pub fn resolve_model_select_permissions(
/// re-add in future. Because this function takes the `data_connector_field_mappings` as an input, /// re-add in future. Because this function takes the `data_connector_field_mappings` as an input,
/// many of the errors thrown in `resolve_model_predicate` are pushed out. /// many of the errors thrown in `resolve_model_predicate` are pushed out.
pub(crate) fn resolve_model_predicate_with_type( pub(crate) fn resolve_model_predicate_with_type(
flags: &open_dds::flags::Flags,
model_predicate: &open_dds::permissions::ModelPredicate, model_predicate: &open_dds::permissions::ModelPredicate,
type_name: &Qualified<CustomTypeName>, type_name: &Qualified<CustomTypeName>,
object_type_representation: &object_relationships::ObjectTypeWithRelationships, object_type_representation: &object_relationships::ObjectTypeWithRelationships,
@ -385,7 +391,10 @@ pub(crate) fn resolve_model_predicate_with_type(
ValueExpression::Literal(json_value.clone()) ValueExpression::Literal(json_value.clone())
} }
open_dds::permissions::ValueExpression::SessionVariable(session_variable) => { open_dds::permissions::ValueExpression::SessionVariable(session_variable) => {
ValueExpression::SessionVariable(session_variable.clone()) ValueExpression::SessionVariable(hasura_authn_core::SessionVariableReference {
name: session_variable.clone(),
passed_as_json: flags.json_session_variables,
})
} }
}; };
@ -641,6 +650,7 @@ pub(crate) fn resolve_model_predicate_with_type(
}?; }?;
let target_model_predicate = resolve_model_predicate_with_type( let target_model_predicate = resolve_model_predicate_with_type(
flags,
nested_predicate, nested_predicate,
&target_model.inner.data_type, &target_model.inner.data_type,
target_object_type, target_object_type,
@ -682,6 +692,7 @@ pub(crate) fn resolve_model_predicate_with_type(
open_dds::permissions::ModelPredicate::Not(predicate) => { open_dds::permissions::ModelPredicate::Not(predicate) => {
let resolved_predicate = resolve_model_predicate_with_type( let resolved_predicate = resolve_model_predicate_with_type(
flags,
predicate, predicate,
type_name, type_name,
object_type_representation, object_type_representation,
@ -702,6 +713,7 @@ pub(crate) fn resolve_model_predicate_with_type(
let mut resolved_predicates = Vec::new(); let mut resolved_predicates = Vec::new();
for predicate in predicates { for predicate in predicates {
resolved_predicates.push(resolve_model_predicate_with_type( resolved_predicates.push(resolve_model_predicate_with_type(
flags,
predicate, predicate,
type_name, type_name,
object_type_representation, object_type_representation,
@ -723,6 +735,7 @@ pub(crate) fn resolve_model_predicate_with_type(
let mut resolved_predicates = Vec::new(); let mut resolved_predicates = Vec::new();
for predicate in predicates { for predicate in predicates {
resolved_predicates.push(resolve_model_predicate_with_type( resolved_predicates.push(resolve_model_predicate_with_type(
flags,
predicate, predicate,
type_name, type_name,
object_type_representation, object_type_representation,

View File

@ -12,6 +12,7 @@ use crate::types::subgraph::Qualified;
use crate::helpers::typecheck; use crate::helpers::typecheck;
use crate::stages::object_types; use crate::stages::object_types;
use crate::ValueExpressionOrPredicate;
/// resolve type permissions /// resolve type permissions
pub fn resolve( pub fn resolve(
@ -54,6 +55,7 @@ pub fn resolve(
output_type_permission, output_type_permission,
)?; )?;
object_type.type_input_permissions = resolve_input_type_permission( object_type.type_input_permissions = resolve_input_type_permission(
&metadata_accessor.flags,
&object_type.object_type, &object_type.object_type,
output_type_permission, output_type_permission,
)?; )?;
@ -97,6 +99,7 @@ pub fn resolve_output_type_permission(
} }
pub(crate) fn resolve_input_type_permission( pub(crate) fn resolve_input_type_permission(
flags: &open_dds::flags::Flags,
object_type_representation: &object_types::ObjectTypeRepresentation, object_type_representation: &object_types::ObjectTypeRepresentation,
type_permissions: &TypePermissionsV1, type_permissions: &TypePermissionsV1,
) -> Result<BTreeMap<Role, TypeInputPermission>, TypeInputPermissionError> { ) -> Result<BTreeMap<Role, TypeInputPermission>, TypeInputPermissionError> {
@ -133,10 +136,23 @@ pub(crate) fn resolve_input_type_permission(
); );
} }
}; };
let resolved_value = match &value {
open_dds::permissions::ValueExpression::Literal(literal) => {
ValueExpressionOrPredicate::Literal(literal.clone())
}
open_dds::permissions::ValueExpression::SessionVariable(session_variable) => {
ValueExpressionOrPredicate::SessionVariable(
hasura_authn_core::SessionVariableReference {
name: session_variable.clone(),
passed_as_json: flags.json_session_variables,
},
)
}
};
resolved_field_presets.insert( resolved_field_presets.insert(
field_name.clone(), field_name.clone(),
FieldPresetInfo { FieldPresetInfo {
value: value.clone(), value: resolved_value,
deprecated: field_definition.deprecated.clone(), deprecated: field_definition.deprecated.clone(),
}, },
); );

View File

@ -1,12 +1,12 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use open_dds::{ use open_dds::{
permissions::{Role, TypeOutputPermission, ValueExpression}, permissions::{Role, TypeOutputPermission},
types::Deprecated, types::Deprecated,
}; };
use crate::stages::object_types;
use crate::Qualified; use crate::Qualified;
use crate::{stages::object_types, ValueExpressionOrPredicate};
use open_dds::types::{CustomTypeName, FieldName}; use open_dds::types::{CustomTypeName, FieldName};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::ops::Deref; use std::ops::Deref;
@ -43,7 +43,7 @@ pub struct TypeInputPermission {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct FieldPresetInfo { pub struct FieldPresetInfo {
pub value: ValueExpression, pub value: ValueExpressionOrPredicate,
pub deprecated: Option<Deprecated>, pub deprecated: Option<Deprecated>,
} }

View File

@ -4,12 +4,12 @@ use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum ValueExpression { pub enum ValueExpression {
Literal(serde_json::Value), Literal(serde_json::Value),
SessionVariable(open_dds::session_variables::SessionVariable), SessionVariable(open_dds::session_variables::SessionVariableReference),
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum ValueExpressionOrPredicate { pub enum ValueExpressionOrPredicate {
Literal(serde_json::Value), Literal(serde_json::Value),
SessionVariable(open_dds::session_variables::SessionVariable), SessionVariable(open_dds::session_variables::SessionVariableReference),
BooleanExpression(Box<model_permissions::ModelPredicate>), BooleanExpression(Box<model_permissions::ModelPredicate>),
} }

View File

@ -829,9 +829,12 @@ input_file: crates/metadata-resolve/tests/passing/boolean_expression_type/partia
nullable: true, nullable: true,
}, },
value: SessionVariable( value: SessionVariable(
SessionVariable( SessionVariableReference {
"x-hasura-user-id", name: SessionVariableName(
), "x-hasura-user-id",
),
passed_as_json: false,
},
), ),
deprecated: None, deprecated: None,
}, },

View File

@ -818,9 +818,12 @@ input_file: crates/metadata-resolve/tests/passing/missing_subgraph_when_ignoring
nullable: false, nullable: false,
}, },
value: SessionVariable( value: SessionVariable(
SessionVariable( SessionVariableReference {
"x-hasura-user-id", name: SessionVariableName(
), "x-hasura-user-id",
),
passed_as_json: false,
},
), ),
deprecated: None, deprecated: None,
}, },

View File

@ -3254,6 +3254,10 @@
"allow_partial_supergraph": { "allow_partial_supergraph": {
"default": false, "default": false,
"type": "boolean" "type": "boolean"
},
"json_session_variables": {
"default": false,
"type": "boolean"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -3294,7 +3298,8 @@
"disallow_scalar_type_names_conflicting_with_inbuilt_types": false, "disallow_scalar_type_names_conflicting_with_inbuilt_types": false,
"propagate_boolean_expression_deprecation_status": false, "propagate_boolean_expression_deprecation_status": false,
"require_unique_command_graphql_names": false, "require_unique_command_graphql_names": false,
"allow_partial_supergraph": false "allow_partial_supergraph": false,
"json_session_variables": false
}, },
"allOf": [ "allOf": [
{ {
@ -3346,7 +3351,8 @@
"disallow_scalar_type_names_conflicting_with_inbuilt_types": false, "disallow_scalar_type_names_conflicting_with_inbuilt_types": false,
"propagate_boolean_expression_deprecation_status": false, "propagate_boolean_expression_deprecation_status": false,
"require_unique_command_graphql_names": false, "require_unique_command_graphql_names": false,
"allow_partial_supergraph": false "allow_partial_supergraph": false,
"json_session_variables": false
}, },
"allOf": [ "allOf": [
{ {
@ -3388,7 +3394,8 @@
"disallow_scalar_type_names_conflicting_with_inbuilt_types": false, "disallow_scalar_type_names_conflicting_with_inbuilt_types": false,
"propagate_boolean_expression_deprecation_status": false, "propagate_boolean_expression_deprecation_status": false,
"require_unique_command_graphql_names": false, "require_unique_command_graphql_names": false,
"allow_partial_supergraph": false "allow_partial_supergraph": false,
"json_session_variables": false
}, },
"allOf": [ "allOf": [
{ {

View File

@ -32,6 +32,9 @@ pub struct Flags {
#[opendd(default, rename = "allow_partial_supergraph")] #[opendd(default, rename = "allow_partial_supergraph")]
pub allow_partial_supergraph: bool, pub allow_partial_supergraph: bool,
#[opendd(default, rename = "json_session_variables")]
pub json_session_variables: bool,
} }
impl Flags { impl Flags {
@ -46,6 +49,7 @@ impl Flags {
propagate_boolean_expression_deprecation_status: false, propagate_boolean_expression_deprecation_status: false,
require_unique_command_graphql_names: false, require_unique_command_graphql_names: false,
allow_partial_supergraph: false, allow_partial_supergraph: false,
json_session_variables: false,
} }
} }

View File

@ -8,7 +8,7 @@ use crate::{
commands::CommandName, commands::CommandName,
models::ModelName, models::ModelName,
relationships::RelationshipName, relationships::RelationshipName,
session_variables::SessionVariable, session_variables::SessionVariableName,
traits, traits,
types::{CustomTypeName, FieldName, OperatorName}, types::{CustomTypeName, FieldName, OperatorName},
}; };
@ -649,7 +649,7 @@ pub enum ValueExpression {
#[schemars(title = "Literal")] #[schemars(title = "Literal")]
Literal(JsonValue), Literal(JsonValue),
#[schemars(title = "SessionVariable")] #[schemars(title = "SessionVariable")]
SessionVariable(SessionVariable), SessionVariable(SessionVariableName),
} }
#[derive( #[derive(
@ -663,7 +663,7 @@ pub enum ValueExpressionOrPredicate {
#[schemars(title = "Literal")] #[schemars(title = "Literal")]
Literal(JsonValue), Literal(JsonValue),
#[schemars(title = "SessionVariable")] #[schemars(title = "SessionVariable")]
SessionVariable(SessionVariable), SessionVariable(SessionVariableName),
#[schemars(title = "BooleanExpression")] #[schemars(title = "BooleanExpression")]
BooleanExpression(Box<ModelPredicate>), BooleanExpression(Box<ModelPredicate>),
} }

View File

@ -7,23 +7,33 @@ use std::str::FromStr;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Used to represent a reference to a session variable,
/// where one is required in the IR
#[derive(Debug, Clone, Hash, PartialEq, Eq, JsonSchema, Serialize, Deserialize)]
#[schemars(rename = "OpenDdSessionVariableReference")]
pub struct SessionVariableReference {
pub name: SessionVariableName,
pub passed_as_json: bool,
}
/// Used to represent the name of a session variable, like /// Used to represent the name of a session variable, like
/// "x-hasura-role". /// "x-hasura-role".
#[derive(Debug, Clone, Hash, PartialEq, Eq, JsonSchema, Serialize, Deserialize)] #[derive(Debug, Clone, Hash, PartialEq, Eq, JsonSchema, Serialize, Deserialize)]
#[schemars(rename = "OpenDdSessionVariable")] #[schemars(rename = "OpenDdSessionVariable")]
pub struct SessionVariable(Cow<'static, str>); pub struct SessionVariableName(Cow<'static, str>);
pub const SESSION_VARIABLE_ROLE: SessionVariable = SessionVariable(Cow::Borrowed("x-hasura-role")); pub const SESSION_VARIABLE_ROLE: SessionVariableName =
SessionVariableName(Cow::Borrowed("x-hasura-role"));
impl FromStr for SessionVariable { impl FromStr for SessionVariableName {
type Err = Infallible; type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(SessionVariable(s.trim().to_lowercase().into())) Ok(SessionVariableName(s.trim().to_lowercase().into()))
} }
} }
impl std::fmt::Display for SessionVariable { impl std::fmt::Display for SessionVariableName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0) write!(f, "{}", self.0)
} }
@ -54,12 +64,12 @@ mod tests {
fn serialize_and_deserialize_session_variable() { fn serialize_and_deserialize_session_variable() {
let mut session_variables = HashMap::new(); let mut session_variables = HashMap::new();
session_variables.insert( session_variables.insert(
SessionVariable("test-role".into()), SessionVariableName("test-role".into()),
SessionVariableValue("test-role".into()), SessionVariableValue("test-role".into()),
); );
let json_str = serde_json::to_string(&session_variables).unwrap(); let json_str = serde_json::to_string(&session_variables).unwrap();
let parsed_from_string: HashMap<SessionVariable, SessionVariableValue> = let parsed_from_string: HashMap<SessionVariableName, SessionVariableValue> =
serde_json::from_str(json_str.trim()).unwrap(); serde_json::from_str(json_str.trim()).unwrap();
assert_eq!(parsed_from_string, session_variables); assert_eq!(parsed_from_string, session_variables);