Skip to content

Commit

Permalink
jsonapi: Check relationship's output type permission while building c…
Browse files Browse the repository at this point in the history
…atalog; fixes OpenAPI schema generation (#1434)

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

### What

While generating the json:api catalog for a given role, include
relationships for an object type only if the relationship's output type
is accessible to the role. This would enable proper open api schema
generation and permission check during json:api validation phase.

### How

<!-- How is it trying to accomplish it (what are the implementation
steps)? -->
Include the relationship in the map after checking the role's
accessibility to the type.

V3_GIT_ORIGIN_REV_ID: 5c7b95d7118a665783d1b765198648d876ad684a
  • Loading branch information
rakeshkky authored and hasura-bot committed Dec 10, 2024
1 parent 4df58b5 commit 1e38a56
Show file tree
Hide file tree
Showing 5 changed files with 433 additions and 13 deletions.
4 changes: 4 additions & 0 deletions v3/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ customizations:

### Fixed

- Fixed the `include` query parameter and `included` response field in JSON:API
OpenAPI schema generation. These now honor type permissions for the role in
relationship fields.

- Fixed a bug where commands with array return types would not build when header
forwarding was in effect.

Expand Down
68 changes: 55 additions & 13 deletions v3/crates/jsonapi/src/catalog/object_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use crate::types::ObjectTypeWarning;
use hasura_authn_core::Role;
use indexmap::IndexMap;
use metadata_resolve::{
ObjectTypeWithRelationships, Qualified, QualifiedBaseType, QualifiedTypeName,
QualifiedTypeReference, ScalarTypeRepresentation,
unwrap_custom_type_name, ObjectTypeWithRelationships, Qualified, QualifiedBaseType,
QualifiedTypeName, QualifiedTypeReference, ScalarTypeRepresentation,
};
use open_dds::types::{CustomTypeName, InbuiltType};
use std::collections::BTreeMap;
Expand Down Expand Up @@ -46,20 +46,49 @@ pub fn build_object_type(
// Relationships
let mut type_relationships = IndexMap::new();
for (_, relationship_field) in &object_type.relationship_fields {
let target = match &relationship_field.target {
metadata_resolve::RelationshipTarget::Model(model) => RelationshipTarget::Model {
object_type: model.target_typename.clone(),
relationship_type: model.relationship_type.clone(),
},
// Only track the relationship if its output type is accessible to the role.
let mut target = None;
match &relationship_field.target {
metadata_resolve::RelationshipTarget::Model(model) => {
if object_type_permission_access(role, &model.target_typename, object_types) {
target = Some(RelationshipTarget::Model {
object_type: model.target_typename.clone(),
relationship_type: model.relationship_type.clone(),
});
}
}
metadata_resolve::RelationshipTarget::ModelAggregate(model_aggregate) => {
let target_type = model_aggregate.target_typename.clone();
RelationshipTarget::ModelAggregate(target_type)
if object_type_permission_access(
role,
&model_aggregate.target_typename,
object_types,
) {
target = Some(RelationshipTarget::ModelAggregate(
model_aggregate.target_typename.clone(),
));
}
}
metadata_resolve::RelationshipTarget::Command(command) => {
let track_this_relationship = if let Some(target_object_type) =
unwrap_custom_type_name(&command.target_type)
{
// For command relationship of a custom type, check if type exists.
object_type_permission_access(role, target_object_type, object_types)
} else {
// The output type of this command relationship is not a custom type; it is a built-in type (scalar).
// Track it.
true
};
if track_this_relationship {
target = Some(RelationshipTarget::Command {
type_reference: command.target_type.clone(),
});
}
}
metadata_resolve::RelationshipTarget::Command(command) => RelationshipTarget::Command {
type_reference: command.target_type.clone(),
},
};
type_relationships.insert(relationship_field.relationship_name.clone(), target);
if let Some(target) = target {
type_relationships.insert(relationship_field.relationship_name.clone(), target);
}
}

Ok(ObjectType {
Expand All @@ -68,6 +97,19 @@ pub fn build_object_type(
})
}

// Check if object_type is accessible to given role
fn object_type_permission_access(
role: &Role,
type_name: &Qualified<CustomTypeName>,
object_types: &BTreeMap<Qualified<CustomTypeName>, ObjectTypeWithRelationships>,
) -> bool {
let mut accessible = false;
if let Some(object_type) = object_types.get(type_name) {
accessible = object_type.type_output_permissions.contains_key(role);
}
accessible
}

// turn an OpenDD type into a type representation
fn type_from_type_representation(
qualified_type_reference: &QualifiedTypeReference,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,138 @@ expression: generated_openapi
}
}
}
},
"included": {
"type": "array",
"items": {
"type": "object",
"anyOf": [
{
"type": "object",
"required": [
"id",
"_type",
"attributes"
],
"properties": {
"_type": {
"enum": [
"default_Artist"
]
},
"attributes": {
"type": "object",
"properties": {
"Name": {
"type": "object"
}
}
},
"id": {
"type": "string"
},
"relationships": {
"type": "object",
"properties": {
"Albums": {
"type": "object",
"required": [
"data"
],
"properties": {
"data": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"_type"
],
"properties": {
"_type": {
"enum": [
"default_Album"
]
},
"id": {
"type": "string"
}
}
}
}
}
}
}
}
}
},
{
"type": "object",
"required": [
"id",
"_type",
"attributes"
],
"properties": {
"_type": {
"enum": [
"default_Track"
]
},
"attributes": {
"type": "object",
"properties": {
"AlbumId": {
"type": "object"
},
"Milliseconds": {
"type": "object"
},
"Name": {
"type": "object"
},
"TrackId": {
"type": "object"
}
}
},
"id": {
"type": "string"
},
"relationships": {
"type": "object",
"properties": {
"Album": {
"type": "object",
"required": [
"data"
],
"properties": {
"data": {
"type": "object",
"required": [
"id",
"_type"
],
"properties": {
"_type": {
"enum": [
"default_Album"
]
},
"id": {
"type": "string"
}
}
}
}
}
}
}
}
}
]
}
}
}
}
Expand Down Expand Up @@ -353,6 +485,68 @@ expression: generated_openapi
"items": {
"type": "object",
"anyOf": [
{
"type": "object",
"required": [
"id",
"_type",
"attributes"
],
"properties": {
"_type": {
"enum": [
"default_Author"
]
},
"attributes": {
"type": "object",
"properties": {
"author_id": {
"type": "object"
},
"last_name": {
"type": "string"
}
}
},
"id": {
"type": "string"
},
"relationships": {
"type": "object",
"properties": {
"articles": {
"type": "object",
"required": [
"data"
],
"properties": {
"data": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"_type"
],
"properties": {
"_type": {
"enum": [
"default_Article"
]
},
"id": {
"type": "string"
}
}
}
}
}
}
}
}
}
},
{
"type": "object",
"required": [
Expand Down Expand Up @@ -421,6 +615,42 @@ expression: generated_openapi
}
}
},
"default_Artist": {
"type": "object",
"properties": {
"Name": {
"type": "object"
}
}
},
"default_Author": {
"type": "object",
"properties": {
"author_id": {
"type": "object"
},
"last_name": {
"type": "string"
}
}
},
"default_Track": {
"type": "object",
"properties": {
"AlbumId": {
"type": "object"
},
"Milliseconds": {
"type": "object"
},
"Name": {
"type": "object"
},
"TrackId": {
"type": "object"
}
}
},
"default_commandArticle": {
"type": "object",
"properties": {
Expand Down
Loading

0 comments on commit 1e38a56

Please sign in to comment.